谈谈JavaScript异步函数发展历程


Posted in Javascript onSeptember 29, 2015

《The Evolution of Asynchronous JavaScript》外文梳理了JavaScript异步函数的发展历程,首先通过回调函数实现异步,之后又经历了Promise/A+、生成器函数,而未来将是async函数的。感谢景庄对该文章的翻译,内容如下:

现在让我们一起来回顾这些年来JavaScript异步函数的发展历程吧。

回调函数Callbacks

似乎一切应该从回调函数开始谈起。

异步JavaScript

正如我们所知道的那样,在JavaScript中,异步编程方式只能通过JavaScript语言中的一等公民函数才能完成:这种方式意味着我们可以将一个函数作为另一个函数的参数,在这个函数的内部可以调用被传递进来的函数(即回调函数)。这也正是回调函数诞生的原因:如果你将一个函数作为参数传递给另一个函数(此时它被称为高阶函数),那么在函数内部, 你可以调用这个函数来完成相应的任务。回调函数没有返回值(不要试图用return),仅仅被用来在函数内部执行某些动作。 看一个例子:

Something.save(function(err) {  
 if (err) { 
  //error handling 
  return; // 没有返回值 
 } 
 console.log('success'); 
});

上面的例子中我们演示了一个错误优先的回调函数(error-first callbacks),这也是Node.js本身的特点之一, Node.js中所有的核心模块和NPM仓库中的大部分模块在编写时都会遵循这个特点。

过度使用回调函数所会遇到的挑战:

如果不能合理的组织代码,非常容易造成回调地狱(callback hell),这会使得你的代码很难被别人所理解。
很容易遗漏错误处理代码。

无法使用return语句返回值,并且也不能使用throw关键字。

也正是基于这些原因,在JavaScript世界中,一直都在寻找着能够让异步JavaScript开发变得更简单的可行的方案。

一个可行的解决方案之一是async模块。如果你和回调函数打过很久的交道, 你也许会深刻地感受到,在JavaScript中如果想要让某些事并行执行,或是串行执行,甚至是使用异步函数来映射(mapping) 数组中的元素使用异步函数有多复杂。所以,感谢 Caolan McMahon写了async模块来解决这些问题。

使用async模块,你可以轻松地以下面这种方式编写代码:

async.map([1, 2, 3], AsyncSquaringLibrary.square,  
 function(err, result){ 
 // result will be [1, 4, 9] 
});

async模块虽然一定程度上带来了便利,但仍然不够简单,代码也不容易阅读,因此Promise出现了。

Promise

当前的JavaScript异步标准可以追溯到2012年,并且直到ES6才变得可用,然而,Promise这个术语却并不是JavaScript 社区所发明的。这个术语来来自于 Daniel P.friedman在1976年的发表的一篇文章。

一个Promise代表的是一个异步操作的最终结果。

现在我们使用Promise来完成上面代码所完成的任务,Promise风格的代码如下:

Something.save()  
 .then(function() { 
  console.log('success'); 
 }) 
 .catch(function() { 
  //error handling 
 })

你会发现,Promise中也利用了回调函数。在then和catch方法中都传入了一个回调函数,分别在Promise被满足和被拒绝时执行。Promise函数的另一个优点是它能够被链接起来完成一系列任务。例如,你可以这样写代码:

saveSomething()  
 .then(updateOtherthing) 
 .then(deleteStuff)  
 .then(logResults);

当你没有现成的Promise时,你可能需要借助一些Promise库,一个流行的选择是使用 bluebird。 这些库可能会提供比原生方案更多的功能,并且不局限于Promise/A+标准所规定的特性。

但是你为什么不用糖方法(sugar methods)呢?建议你首先阅读 Promise: The Extension Problem这篇文章。更多关于Promise的信息,可以参考 Promise/A+标准。

你可能会问:如果大部分的库只暴露了回调的接口的话,那么我该如何使用Promise?

嗯,这个很简单,此时你唯一需要做的就是使用Promise来包裹含有回调的那个函数调用体。例如:

回调风格的代码可能是这样的:

function saveToTheDb(value) { 
  db.values.insert(value, function (err, user) { 
    if (err) throw err; 
 
    // todo: insert user to db 
  }); 
}

现在我们将其改成支持Promise风格调用的代码:

function saveToTheDb(value) {  
  return new Promise(function(resolve, reject) { 
    db.values.insert(value, function(err, user) { // remember error first ;) 
      if (err) { 
        return reject(err); // don't forget to return here 
      } 
      resolve(user); 
    }) 
  } 
}

已经有相当一部分的库或框架同时支持者两种方式了,即同时提供了回调风格和Promise风格的API接口。那么现在, 如果你也想对外提供一个库,最佳实践也是同时提供两种方式的接口。你可以轻松的使用如下方式来达到这个目的:

function foo(cb) {  
 if (cb) { 
  return cb(); 
 } 
 return new Promise(function (resolve, reject) { 
 
 }); 
}

或者更简单些,你可以从只提供Promise风格的接口开始后,并使用诸如 callbackify这样的工具来达到向后兼容的目的。其实Callbackify所做的工作和上面的代码片段类似,但在实现上使用了一个更通用的方法, 我建议你可以去阅读Callbackify的源代码。

生成器Generators/ yield

JavaScript 生成器是个相对较新的概念, 它是ES6(也被称为ES2015)的新特性。想象下面这样的一个场景:

当你在执行一个函数的时候,你可以在某个点暂停函数的执行,并且做一些其他工作,然后再返回这个函数继续执行, 甚至是携带一些新的值,然后继续执行。
上面描述的场景正是JavaScript生成器函数所致力于解决的问题。当我们调用一个生成器函数的时候,它并不会立即执行, 而是需要我们手动的去执行迭代操作(next方法)。也就是说,你调用生成器函数,它会返回给你一个迭代器。迭代器会遍历每个中断点。

function* foo () {  
 var index = 0; 
 while (index < 2) { 
  yield index++; //暂停函数执行,并执行yield后的操作 
 } 
} 
var bar = foo(); // 返回的其实是一个迭代器 
console.log(bar.next());  // { value: 0, done: false }  
console.log(bar.next());  // { value: 1, done: false }  
console.log(bar.next());  // { value: undefined, done: true }

 更进一步的,如果你想更轻松的使用生成器函数来编写异步JavaScript代码,我们可以使用 co 这个库,co是著名的tj大神写的。

Co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
使用co,前面的示例代码,我们可以使用下面的代码来改写:

co(function* (){  
 yield Something.save(); 
}).then(function() { 
 // success 
}) 
.catch(function(err) { 
 //error handling 
});

你可能会问:如何实现并行操作呢?答案可能比你想象的简单,如下(其实它就是Promise.all而已):

yield [Something.save(), Otherthing.save()];  
Async/ await

在ES7(还未正式标准化)中引入了Async函数的概念,目前如果你想要使用的话,只能借助于babel 这样的语法转换器将其转为ES5代码。(提醒一点:我们现在讨论的是async关键字,而不是NPM中的async包)。

简而言之,使用async关键字,你可以轻松地达成之前使用生成器和co函数所做到的工作。当然,除了hack之外。

也许你会问,是否在ES7中有了async关键字,yield就变得不是那么重要了?

实际上,使用yield实现异步也不过是一种hack罢了,yield意味着懒次序(lazy sequences)和迭代器。 而await能够完美的分离这两点,首先让yield用于其最初的目的,其次使用await来执行异步操作。

在这背后,async函数实际使用的是Promise,也就是为什么async函数会返回一个Promise的原因。

因此,我们使用async函数来完成类似于前面代码所完成的工作,可以使用下面这样的方式来重新编写代码:

async function save(Something) {  
 try { 
  await Something.save(); // 等待await后面的代码执行完,类似于yield 
 } catch (ex) { 
  //error handling 
 } 
 console.log('success'); 
}

正如你看到的那样,使用async函数,你需要在函数声明的最前面加上async关键字。这之后,你可以在函数内部使用await关键字了,作用和之前的yield作用是类似的。

使用async函数完成并行任务与yiled的方式非常的相似,唯一不同的是,此时Promise.all不再是隐式的,你需要显示的调用它:

async function save(Something) {  
  await Promise.all[Something.save(), Otherthing.save()] 
}

Koa也支持async函数,如果你也在使用koa,那么你现在就可以借助babel使用这一特性了。

import koa from koa;  
let app = koa(); 
app.experimental = true; 
app.use(async function (){  
  this.body = await Promise.resolve('Hello Reader!') 
}) 
app.listen(3000);

以上内容给大家分享了JavaScript异步函数发展历程,希望对大家有所帮助。

Javascript 相关文章推荐
sina的lightbox效果。
Jan 09 Javascript
基于jquery的下拉框改变动态添加和删除表格实现代码
Sep 12 Javascript
jQuery树形下拉菜单特效代码分享
Aug 15 Javascript
javascript计时器编写过程与实现方法
Feb 29 Javascript
原生javascript实现图片放大镜效果
Jan 18 Javascript
Angular 4.x 路由快速入门学习
May 03 Javascript
Javascript实现时间倒计时效果
Jul 15 Javascript
react+redux的升级版todoList的实现
Dec 18 Javascript
微信小程序之裁剪图片成圆形的实现代码
Oct 11 Javascript
解决LayUI加上form.render()下拉框和单选以及复选框不出来的问题
Sep 27 Javascript
Vue通过for循环随机生成不同的颜色或随机数的实例
Nov 09 Javascript
Taro小程序自定义顶部导航栏功能的实现
Dec 17 Javascript
JavaScript 对象深入学习总结(经典)
Sep 29 #Javascript
深入浅析JavaScript中对事件的三种监听方式
Sep 29 #Javascript
jQuery中的ajax async同步和异步详解
Sep 29 #Javascript
网页从弹窗页面单选框传值至父页面代码分享
Sep 29 #Javascript
pace.js页面加载进度条插件
Sep 29 #Javascript
js强制把网址设为默认首页
Sep 29 #Javascript
jQuery EasyUI Dialog拖不下来如何解决
Sep 28 #Javascript
You might like
PHP MemCached 高级缓存应用代码
2010/08/05 PHP
php读取EXCEL文件 php excelreader读取excel文件
2012/12/06 PHP
Android App中DrawerLayout抽屉效果的菜单编写实例
2016/03/21 PHP
php函数传值的引用传递注意事项分析
2016/06/25 PHP
php中10个不同等级压缩优化图片操作示例
2016/11/14 PHP
使用PHP连接多种数据库的实现代码(mysql,access,sqlserver,Oracle)
2016/12/21 PHP
php unicode编码和字符串互转的方法
2020/08/12 PHP
PHP实现读取文件夹及批量重命名文件操作示例
2019/04/15 PHP
对YUI扩展的Gird组件 Part-2
2007/03/10 Javascript
Jquery下判断Id是否存在的代码
2011/01/06 Javascript
JQuery中关于jquery.js与jquery.min.js的比较探讨
2013/05/15 Javascript
jquery text(),val(),html()方法区别总结
2013/11/04 Javascript
ie与ff下的event事件使用介绍
2013/11/25 Javascript
对Jquery中的ajax再封装,简化操作示例
2014/02/12 Javascript
Express作者TJ告别Node.js奔向Go
2014/07/14 Javascript
JS实现中国公民身份证号码有效性验证
2017/02/20 Javascript
vuejs使用$emit和$on进行组件之间的传值的示例
2017/10/04 Javascript
bootstrap+jquery项目引入文件报错的解决方法
2018/01/22 jQuery
vue通过点击事件读取音频文件的方法
2018/05/30 Javascript
Python记录详细调用堆栈日志的方法
2015/05/05 Python
Python的Flask框架中SQLAlchemy使用时的乱码问题解决
2015/11/07 Python
Django rest framework实现分页的示例
2018/05/24 Python
python实现彩票系统
2020/06/28 Python
详解10个可以快速用Python进行数据分析的小技巧
2019/06/24 Python
通过css3动画和opacity透明度实现呼吸灯效果
2019/08/09 HTML / CSS
AE美国鹰美国官方网站:American Eagle Outfitters
2016/08/22 全球购物
Clarks鞋美国官网:全球领军鞋履品牌
2017/05/13 全球购物
KIEHL’S科颜氏官方旗舰店:源自美国的顶级护肤品牌
2018/06/07 全球购物
吉列剃须刀美国官网:Gillette美国
2018/07/13 全球购物
《郑和远航》教学反思
2014/04/16 职场文书
反腐倡廉标语
2014/06/24 职场文书
我爱祖国演讲稿
2014/09/02 职场文书
农村党员学习党的群众路线教育实践活动心得体会
2014/11/04 职场文书
大学生求职自荐信
2015/03/24 职场文书
图书馆义工感想
2015/08/07 职场文书
Go 语言下基于Redis分布式锁的实现方式
2021/06/28 Golang