谈谈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 相关文章推荐
IE FF OPERA都可用的弹出层实现代码
Sep 29 Javascript
用document.documentElement取代document.body的原因分析
Nov 12 Javascript
jquery easyui 对于开始时间小于结束时间的判断示例
Mar 22 Javascript
PHPMyAdmin导入时提示文件大小超出PHP限制的解决方法
Mar 30 Javascript
JS iFrame加载慢怎么解决
May 13 Javascript
最棒的Angular2表格控件
Aug 10 Javascript
jQuery实现侧浮窗与中浮窗切换效果的方法
Sep 05 Javascript
JavaScript计算值然后把值嵌入到html中的实现方法
Oct 29 Javascript
用Vue.js实现监听属性的变化
Nov 17 Javascript
Angular 2 ngForm中的ngModel、[ngModel]和[(ngModel)]的写法
Jun 29 Javascript
微信小程序定位当前城市的方法
Jul 19 Javascript
使用angularjs.foreach时return的问题解决
Sep 30 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
当年上海收录机产品生产,进口和价格情况
2021/03/04 无线电
解析strtr函数的效率问题
2013/06/26 PHP
PHP中使用file_get_contents post数据代码例子
2015/02/13 PHP
php生成数字字母的验证码图片
2015/07/14 PHP
ThinkPHP表单令牌错误的相关解决方法分析
2016/05/20 PHP
php双层循环(九九乘法表)
2017/10/23 PHP
javascript 异常处理使用总结
2009/06/21 Javascript
基于Jquery的淡入淡出的特效基础练习
2010/12/13 Javascript
js实现div弹出层的方法
2014/11/20 Javascript
JavaScript中的Math.atan2()方法使用详解
2015/06/15 Javascript
原生js实现放大镜效果
2017/01/11 Javascript
js实现仿购物车加减效果
2017/03/01 Javascript
JS+html5 canvas实现的简单绘制折线图效果示例
2017/03/13 Javascript
jQuery输入框密码的显示隐藏【代码分享】
2017/04/29 jQuery
jQuery实现切换隐藏与显示同时切换图标功能
2017/10/29 jQuery
解析Vue 2.5的Diff算法
2017/11/28 Javascript
解析Angular 2+ 样式绑定方式
2018/01/15 Javascript
使用vue-cli3新建一个项目并写好基本配置(推荐)
2019/04/24 Javascript
移动端吸顶fixbar的解决方案详解
2019/07/17 Javascript
layer 关闭指定弹出层的例子
2019/09/25 Javascript
微信小程序云开发获取文件夹下所有文件(推荐)
2019/11/14 Javascript
微信小程序实现横向滚动导航栏效果
2019/12/12 Javascript
vue各种事件监听实例(小结)
2020/06/24 Javascript
JavaScript经典案例之简易计算器
2020/08/24 Javascript
python 实现将文件或文件夹用相对路径打包为 tar.gz 文件的方法
2019/06/10 Python
Python中zip()函数的简单用法举例
2019/09/02 Python
Python拆分大型CSV文件代码实例
2019/10/07 Python
Python 使用 PyQt5 开发的关机小工具分享
2020/07/16 Python
德国二手设计师时装和复古时装跳蚤市场:Mädchenflohmarkt
2020/11/09 全球购物
试解释COMMIT操作和ROLLBACK操作的语义
2014/07/25 面试题
商务会议邀请函
2014/01/09 职场文书
销售内勤岗位职责
2014/04/15 职场文书
教师节晚会主持词
2015/06/30 职场文书
elementui的el-popover修改样式不生效的解决
2021/06/30 Javascript
使用Redis实现实时排行榜功能
2021/07/02 Redis
java实现web实时消息推送的七种方案
2022/07/23 Java/Android