异步流程控制

主要讲3个

  • Promise
  • Generator/yield
  • Async/await

Promise/a+规范

顺序执行的代码和错误有限的回调方式都是js引擎默认支持的,这部分大家会调用接口,无太多变化,而Promise是对callback的思考,或者说改良方案,目前使用非常普遍,这里详细讲解一下。

node里的Promise

promise最早是在commonjs社区提出来的,当时提出了很多规范。比较接受的是promise/A规范。后来人们在这个基础上。提出了promise/A+规范,也就是实际上的业内推行的规范。es6也是采用的这种规范。

The Promise object is used for asynchronous computations. A Promise represents an operation that hasn’t completed yet, but is expected in the future.

Promise对象用于异步技术中。Promise意味着一个还没有完成的操作(许愿),但在未来会完成的(实现)。

异步流程控制 - 图1

在Node.js 0.12里实现9/11,在6.2和7实现100%,中间版本实现了10/11。所以Node.js对Promise的支持是非常好的,0.12之后的绝大部分版本都支持的不错。

Promise 的最大优势是标准,各类异步工具库都认同,未来的 async/await 也基于它,用它封装 API 通用性强,用起来简单。

要想知道node.js有哪些比较好的promise实现,最好的办法就是看一下最知名的bluebird库的benchmark里比较里哪些。

Promise是什么?

A promise is an abstraction for asynchronous programming. It’s an object that proxies for the return value or the exception thrown by a function that has to do some asynchronous processing. — Kris Kowal on JSJ

Promise表示一个异步操作的最终结果。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。

  • 递归,每个异步操作返回的都是promise对象
  • 状态机:三种状态转换,只在promise对象内部可以控制,外部不能改变状态
  • 全局异常处理

定义

  1. var promise = new Promise(function(resolve, reject) {
  2. // do a thing, possibly async, then…
  3. if (/* everything turned out fine */) {
  4. resolve("Stuff worked!");
  5. }
  6. else {
  7. reject(Error("It broke"));
  8. }
  9. });

术语

  • Promises Promise规范自身
  • promise对象 promise对象指的是 Promise 实例对象
  • ES6 Promises 如果想明确表示使用 ECMAScript 6th Edition 的话,可以使用ES6作为前缀(prefix)
  • Promises/A+ Promises/A+。 这是ES6 Promises的前身,是一个社区规范,它和 ES6 Promises 有很多共通的内容。
  • Thenable 类Promise对象。 拥有名为.then方法的对象。

hello promise

给出一个最简单的读写文件的api实例,它是error-first风格的典型api

async/promise/hello.js

  1. // callbacks
  2. var fs = require("fs");
  3. fs.readFile('./package.json', (err, data) => {
  4. if (err) throw err;
  5. console.log(data.toString());
  6. });

下面,我们把它变成promise的简单示例

async/promise/hellopromise.js

  1. // callbacks to promise
  2. var fs = require("fs");
  3. function hello (file) {
  4. return new Promise(function(resolve, reject){
  5. fs.readFile(file, (err, data) => {
  6. if (err) {
  7. reject(err);
  8. } else {
  9. resolve(data.toString())
  10. }
  11. });
  12. });
  13. }
  14. hello('./package.json').then(function(data){
  15. console.log('promise result = ' + data)
  16. }).catch(function(err) {
  17. console.log(err)
  18. })

这二段代码执行效果是一模一样的,唯一的差别是前一种写法是Node.js默认api写法,以回调为主,而后一种写法,通过返回promise对象,在fs.readFile的回调函数,将结果延后处理。

这就是最简单的promise实现

形式

  1. new Promise(function(resolve, reject){
  2. })

参数

  • resolve 解决,进入到下一个流程
  • reject 拒绝,跳转到捕获异常流程

调用

  1. hello('./package.json').then(function(data){
  2. })

全局处理异常

  1. hello('./package.json').then(function(data){
  2. }).catch(function(err) {
  3. })

结论

Promise核心:将callback里的结果延后到then函数里处理或交给全局异常处理

封装api的过程

还是以上面的fs.readFile为例

  1. fs.readFile('./package.json', (err, data) => {
  2. if (err) throw err;
  3. console.log(data.toString());
  4. });

参数处理:除了callback外,其他东西都放到新的函数的参数里

  1. function hello (file) {
  2. ...
  3. }

返回值处理:返回Promise实例对象

  1. function hello (file) {
  2. return new Promise(function(resolve, reject){
  3. ...
  4. });
  5. }

结果处理:通过resolve和reject重塑流程

  1. function hello (file) {
  2. return new Promise(function(resolve, reject){
  3. fs.readFile(file, (err, data) => {
  4. if (err) {
  5. reject(err);
  6. } else {
  7. resolve(data.toString())
  8. }
  9. });
  10. });
  11. }

我们知道所有的Node.js都是error-first的callback形式,通过上面的例子,我们可以肯定是所有的Node.js的API都可以这样来处理,只要它们遵守Promise规范即可。

每个函数的返回值都是Promise对象

为了简化编程复杂性,每个函数的返回值都是Promise对象,这样的约定可以大大的简化编程的复杂。

它可以理解为是递归的变种思想应用,只要是Promise对象,就可以控制状态,就可以支持then方法,参数还是Promise对象,这样就可以无限个Promise对象链接在一起。

  1. // callbacks to promise
  2. var fs = require("fs");
  3. function hello (file) {
  4. return new Promise(function(resolve, reject){
  5. fs.readFile(file, (err, data) => {
  6. if (err) {
  7. reject(err);
  8. } else {
  9. resolve(data.toString())
  10. }
  11. });
  12. });
  13. }
  14. function world (file) {
  15. return new Promise(function(resolve, reject){
  16. fs.readFile(file, (err, data) => {
  17. if (err) {
  18. reject(err);
  19. } else {
  20. resolve(data.toString())
  21. }
  22. });
  23. });
  24. }
  25. function log(data){
  26. return new Promise(function(resolve, reject){
  27. console.log('promise result = ' + data)
  28. resolve(data)
  29. });
  30. }
  31. hello('./package.json').then(log).then(function(){
  32. return hello('./each.js').then(log)
  33. }).catch(function(err) {
  34. console.log(err)
  35. })

这里可以看出

  • helloworldlog 返回单个Promise对象
  • hello('./each.js').then(log) 返回流程链

无论是单个,还是流程链的返回值都是Promise对象,那么它就是一样的。

链式的thenable

每个promose对象都有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。

一般实现,类似于

  1. Promise.prototype.then = function(sucess, fail) {
  2. this.done(sucess);
  3. this.fail(fail);
  4. return this;
  5. };

它的返回值是this,这就是为什么then可以链式操作的原因。

then的2个参数

  • sucess是fulfilled状态的回调函数
  • fail是rejected状态的回调函数

一般都是穿sucess回调函数即可。

状态转换

一个Promise必须处在其中之一的状态:pending, fulfilled 或 rejected.

  • pending: 初始状态, 非 fulfilled 或 rejected.
  • fulfilled: 完成(成功)的操作.
  • rejected: 拒绝(失败)的操作.

这里从pending状态可以切换到fulfill状态,也可以从pengding切换到reject状态,这个状态切换不可逆,且fulfilled和reject两个状态之间是不能互相切换的。

一定要注意的是,只有异步操作的结果,才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

Promise对象可以理解为一个乐高积木,它对下一个流程,传送状态和具体结果。

异步流程控制 - 图2

如果是pending状态,则promise:

  • 可以转换到fulfilled或rejected状态。

如果是fulfilled状态,则promise:

  • 不能转换成任何其它状态。
  • 必须有一个值,且这个值不能被改变。

如果是rejected状态,则promise可以:

  • 不能转换成任何其它状态。
  • 必须有一个原因,且这个值不能被改变。

”值不能被改变”指的是其identity不能被改变,而不是指其成员内容不能被改变。

reject和resove流程再造

前面讲了,每个函数的返回值都是Promise对象,每个Promise对象都有then方法,这是它可以递归思路的解决办法。

那么问题来了,如何在连续的操作步骤里,完成流程再造呢?这其实才是异步流程控制最核心的问题。

我们知道Promise的使用形式如下:

  1. new Promise(function(resolve, reject){
  2. })

下面仍然使用fs的例子,见reflow.js

way 1:简单模式

  1. hello('./package.json').then(function(data){
  2. console.log('way 1:\n')
  3. return new Promise(function(resolve, reject){
  4. console.log('promise result = ' + data)
  5. resolve(data)
  6. });
  7. }).then(function(data){
  8. return new Promise(function(resolve, reject){
  9. resolve('1')
  10. });
  11. }).then(function(data){
  12. console.log(data)
  13. return new Promise(function(resolve, reject){
  14. reject(new Error('reject with custom err'))
  15. });
  16. }).catch(function(err) {
  17. console.log(err)
  18. })

这是一个常规的例子,就是在then里面的promise对象里,通过resolve将流程进行到下一步,在reject的时候抛出异常。这里面的每一个promise对象里都可以这样做,那么是不是这个操作流程就是可控的了?

way 2:嵌套模式

  1. hello('./package.json').then(function(data){
  2. console.log('\n\nway 2:\n')
  3. return new Promise(function(resolve, reject){
  4. console.log('promise result = ' + data)
  5. resolve(data)
  6. }).then(function(data){
  7. return new Promise(function(resolve, reject){
  8. resolve('1')
  9. });
  10. }).catch(function(err) {
  11. console.log(err)
  12. })
  13. }).then(function(data){
  14. console.log(data)
  15. return new Promise(function(resolve, reject){
  16. reject(new Error('reject with custom err'))
  17. });
  18. }).catch(function(err) {
  19. console.log(err)
  20. })

这里的做法是,把第一个then和第二个then合并到一个流程里。这样做的好处是,这个流程也可以考虑单独处理异常。为了某些粒度更新的异步处理,是非常有好处的。

way 3:嵌套模式的refact清晰版

  1. var step1 = function(data){
  2. console.log('\n\nway 3:\n')
  3. return new Promise(function(resolve, reject){
  4. console.log('promise result = ' + data)
  5. resolve(data)
  6. }).then(function(data){
  7. return new Promise(function(resolve, reject){
  8. resolve('1')
  9. });
  10. }).catch(function(err) {
  11. console.log(err)
  12. })
  13. }
  14. var step2 = function(data){
  15. console.log(data)
  16. return new Promise(function(resolve, reject){
  17. reject(new Error('reject with custom err'))
  18. });
  19. }
  20. hello('./package.json').then(step1).then(step2).catch(function(err) {
  21. console.log(err)
  22. })

把每个独立的操作抽成函数,然后函数的返回值是Promise对象,这样就可以在真正的流程链里随意组织了。

它们就好比是积木一样,可以让逻辑更清楚,让代码更具可读性和可维护性。如果再极端点,每个操作都放到独立文件里,变成模块,是不是更爽呢?

way 4:final版,把每个独立的操作放到独立文件里,变成模块

原理: 使用require-directory

根据commonjs规范,require只能引用某一个文件,当一个文件夹里有很多文件,每一个都去require是很麻烦的,require-directory就是一个便捷模块,可以把某个文件夹内的多个文件挂载到一个对象。

原理,递归遍历文件,读取具体文件,如果是遵循commonjs规范的模块,就挂载在它的返回值对象上。

比如reflow/tasks/index.js

  1. var requireDirectory = require('require-directory');
  2. module.exports = requireDirectory(module);

这样reflow/tasks下的所有遵循commonjs规范的模块都可以挂载

reflow/tasks/hello.js

  1. var fs = require("fs");
  2. module.exports = function hello (file) {
  3. return new Promise(function(resolve, reject){
  4. fs.readFile(file, (err, data) => {
  5. if (err) {
  6. reject(err);
  7. } else {
  8. resolve(data.toString())
  9. }
  10. });
  11. });
  12. }

这其实和之前的定义是一模一样的,唯一差别就是变成了模块,使用了module.exports来导出。

其他的step1和step2以此类推,下面我们卡一下具体调用的代码

  1. var tasks = require('./tasks')
  2. tasks.hello('./package.json').then(tasks.step1).then(tasks.step2).catch(function(err) {
  3. console.log(err)
  4. })

给出具体的流程图

异步流程控制 - 图3

首先require('./tasks')获得tasks目录下的所有操作任务定义,然后在下面的Promise流程里处理,可以看出定义和实现分离,让代码有更好的可读性。

如果,这时我们恰好需要调整step1和step2的顺序,是不是非常的简单?

  1. var tasks = require('./tasks')
  2. tasks.hello('./package.json').then(tasks.step2).then(tasks.step1).catch(function(err) {
  3. console.log(err)
  4. })

更多好处,自行体会吧,这里就不做更多解释了。

错误处理

常用的处理方式是全局处理,即所有的异步操作都由一个catch来处理

  1. promise.then(function(result) {
  2. console.log('Got data!', result);
  3. }).catch(function(error) {
  4. console.log('Error occurred!', error);
  5. });

当然,then方法的第二个参数也是可以的

  1. promise.then(function(result) {
  2. console.log('Got data!', result);
  3. }).then(undefined, function(error) {
  4. console.log('Error occurred!', error);
  5. });

如果有多个then配对的reject函数呢?是不是可以更加灵活?这其实就要取决于你的业务复杂程度里。

错误处理最简单的办法是在promise里使用try/catch的语句。在try/catch块中,它可能去捕获异常,并显示处理它:(TODO: 重写个更简单例子)

  1. try {
  2. throw new Error('never will know this happened')
  3. } catch (e) {}

在promises里可以这样写

  1. readFile()
  2. .then(function (data) {
  3. throw new Error('never will know this happened')
  4. })

为了打印errors,这里以简单的.then(null, onRejected)语句为例

  1. readFile()
  2. .then(function (data) {
  3. throw new Error('now I know this happened')
  4. })
  5. .then(null, console.error)

类库包括一些暴露error的其他选项。比如Q就提供了done方法,可以再次跑出error异常的。

链式写法很方便,可以随意组合,

api/catch.js

  1. var p1 = new Promise(function(resolve, reject) {
  2. resolve('Success');
  3. });
  4. p1.then(function(value) {
  5. console.log(value); // "Success!"
  6. return Promise.reject('oh, no!');
  7. }).catch(function(e) {
  8. console.log(e); // "oh, no!"
  9. // return Promise.reject('oh, no! 2');
  10. }).then(function(){
  11. console.log('after a catch the chain is restored');
  12. }, function () {
  13. console.log('Not fired due to the catch');
  14. });

执行

  1. $ node api/catch.js
  2. Success
  3. oh, no!
  4. after a catch the chain is restored

api/catch2.js

  1. var p1 = new Promise(function(resolve, reject) {
  2. resolve('Success');
  3. });
  4. p1.then(function(value) {
  5. console.log(value); // "Success!"
  6. return Promise.reject('oh, no!');
  7. }).catch(function(e) {
  8. console.log(e); // "oh, no!"
  9. return Promise.reject('oh, no! 2');
  10. }).then(function(){
  11. console.log('after a catch the chain is restored');
  12. }, function () {
  13. console.log('Not fired due to the catch');
  14. });

执行

  1. $ node api/catch2.js
  2. Success
  3. oh, no!
  4. Not fired due to the catch

Node.js的promise库

Promise扩展类库除了实现了Promise中定义的规范之外,还增加了自己独自定义的功能。

按字母排序

package repo alias
bluebird petkaantonov/bluebird bb
es6-promise jakearchibald/es6-promise
es6-promise-polyfill [^1] lahmatiy/es6-promise-polyfill
es6-promises Octane/Promise
lie calvinmetcalf/lie
native-promise-only getify/native-promise-only npo
promiscuous RubenVerborgh/promiscuous
promise then/promise then
promiz Zolmeister/promiz
q kriskowal/q
rsvp tildeio/rsvp.js
vow dfilatov/vow
when cujojs/when w

[^1]: based on es6-promise, so excluded from the registery by default

Promise扩展类库数量非常的多,我们只介绍其中两个比较有名的类库。

类库 Q 实现了 Promises 和 Deferreds 等规范。 它自2009年开始开发,还提供了面向Node.js的文件IO API Q-IO 等, 是一个在很多场景下都能用得到的类库。

这个类库除了兼容 Promise 规范之外,还扩展了取消promise对象的运行,取得promise的运行进度,以及错误处理的扩展检测等非常丰富的功能,此外它在实现上还在性能问题下了很大的功夫。

Q 和 Bluebird 这两个类库除了都能在浏览器里运行之外,充实的API reference也是其特征。由于Bluebird的性能比较好,所以我们一般用Bluebird的时候会比较多。

Q等文档里详细介绍了Q的Deferred和jQuery里的Deferred有哪些异同,以及要怎么进行迁移 Coming from jQuery 等都进行了详细的说明。

Bluebird的文档除了提供了使用Promise丰富的实现方式之外,还涉及到了在出现错误时的对应方法以及 Promise中的反模式 等内容。

这两个类库的文档写得都很友好,即使我们不使用这两个类库,阅读一下它们的文档也具有一定的参考价值。

替换bluebird

tj: bluebird is MASSIVE, why not use v8’s?

bluebird是Node.js世界里性能最好的模块,api非常齐全,功能强大,是原生Promise外的不二选择。

安装bluebird模块

  1. $ npm i -S bluebird

见代码hellopromise-bb.js

  1. // callbacks to promise
  2. var fs = require("fs");
  3. var Promise = require("bluebird");
  4. function hello (file) {
  5. return new Promise(function(resolve, reject){
  6. fs.readFile(file, (err, data) => {
  7. if (err) {
  8. reject(err);
  9. } else {
  10. resolve(data.toString())
  11. }
  12. });
  13. });
  14. }
  15. hello('./package.json').then(function(data){
  16. console.log('promise result = ' + data)
  17. }).catch(function(err) {
  18. console.log(err)
  19. })

它和之前的hellopromise.js执行结果是一模一样的,只差一行代码,即

  1. var Promise = require("bluebird");

由此可以看出,Node.js原生的Promise和bluebird的实现是兼容的。只要掌握其中任何一个,几乎是0成本代价就可以学会。

这里用的是var来声明Promise,主要目的是为了当前文件使用,如果是koa或express这样的web项目里,使用全局替换呢?

其实也很简单,使用global全局替换就好,在应用的入口文件app.js里

  1. global.Promise = require("bluebird");

Promisification

Promisification means converting an existing promise-unaware API to a promise-returning API.

这里主要介绍一下bluebird的promisefy和promisifyAll

promisifyAll更彻底,对类方法或者对象方法都可以进行promisify处理,是最简单的包裹promisify的常用手段,比如

  1. var Promise = require("bluebird");
  2. var fs = Promise.promisifyAll(require("fs"));
  3. fs.readFileAsync("./package.json", "utf8").then(function(contents) {
  4. console.log(contents);
  5. }).catch(function(e) {
  6. console.error(e.stack);
  7. });

再来个稍微复杂一些的,下面这个例子有abc 3个方法,每个都是普通函数,通过bluebird的promisifyAll让他变成promise对象,继而完成流程控制。

  1. var Promise = require("bluebird");
  2. var obj = {
  3. a: function(){
  4. console.log('a')
  5. },
  6. b: function(){
  7. console.log('b')
  8. },
  9. c: function(){
  10. console.log('c')
  11. }
  12. }
  13. Promise.promisifyAll(obj);
  14. obj.aAsync().then(obj.bAsync()).then(obj.cAsync()).catch(function(err){
  15. console.log(err)
  16. })

是不是非常简单?

危险常常来自便利处,大量的这样promisifyAll,会不会有性能问题呢?error被bluebird包裹了,我们自己想定制呢?

Promise的5个api

异步流程控制 - 图4

1)构造方法

语法

new Promise( / executor / function(resolve, reject) { … } );

所有Promise只能这样创建,它的2个参数resolve和reject是唯一可以改变对象状态的方法。

  • resolve会让状态从pending切换到fulfilled
  • reject会让状态从pending切换到rejected(可选,不写也不算错)
    • Promise.prototype.then()可以当前操作的reject异常
    • Promise.prototype.catch()可以捕获全局的reject异常

备注:这里的resolve相当于Promise.resolve的别名,reject相当于Promise.reject的别名。

promise/api/a.js

  1. new Promise(function(resolve){
  2. resolve(1);
  3. }).then(function(value){
  4. console.log('new Promise ' + value);
  5. });
  6. Promise.resolve(1).then(function(value){
  7. console.log('Promise.resolve ' + value);
  8. });

这2个示例resolve效果是一样的,可以看出Promise.resolve是便捷用法

promise/api/b.js

  1. var error = new Error('this is a error')
  2. new Promise(function(resolve, reject){
  3. reject(error);
  4. }).catch(function(err){
  5. console.log('new Promise ' + err);
  6. });
  7. Promise.reject(error).catch(function(err){
  8. console.log('Promise.resolve ' + err);
  9. });

这2个示例reject效果是一样的,可以看出Promise.reject是便捷用法

既然resolve和reject都有别名,那么我们能不能不适用构造函数,直接使用便捷用法呢?答案是不可以的,具体如下,见promise/api/c.js

  1. // 以下做法是错误的
  2. new Promise(function(){
  3. return Promise.resolve(1)
  4. }).then(function(value){
  5. console.log('Promise.resolve 1 ' + value);
  6. });

可能有的库会实现,但Node.js的原生Promise是不支持这样的写法的。

想便捷的话,一般采用下面这样的方法

promise/api/d.js

  1. // 以下做法是正确的的
  2. function hello(i){
  3. return Promise.resolve(i)
  4. }
  5. hello(1).then(function(value){
  6. console.log('Promise.resolve 1 ' + value);
  7. });

这种写法可行原因是,Promise.resolve返回的是Promise对象,相当于new Promise(resolve, reject)

但是一定要注意,一旦的函数确定要返回Promise对象,就一定要全部可能分支都要返回Promise对象,不然出了问题非常难定位。

举个简单的例子,i是奇数或偶数做不一样的处理,一定要严谨。

promise/api/e.js

  1. // 奇数和偶数
  2. function hello(i){
  3. if (i % 2 == 0) {
  4. return Promise.resolve(i)
  5. } else {
  6. return Promise.reject(i)
  7. }
  8. }
  9. hello(1).then(function(value){
  10. console.log('Promise.reject 1 ' + value);
  11. });
  12. hello(2).then(function(value){
  13. console.log('Promise.resolve 1 ' + value);
  14. });

其实按照规范Promise.resolve和Promise.reject还有更多用法,其他的给出语法定义,了解一下即可,没有特别需要说明的。

Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable);

Promise.reject(reason);

2)核心方法Promise.prototype.then()

语法

p.then(onFulfilled, onRejected);

p.then(function(value) {
// fulfillment
}, function(reason) {
// rejection
});

3)次核心方法Promise.prototype.catch()

p.catch(onRejected);

p.catch(function(reason) {
// rejection
});

4)工具方法

  • Promise.all(iterable)
  • Promise.race(iterable)

Promise.all 在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理, 与之相对的是 Promise.race 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。

简单点就说,all是所有都执行完成,再执行then,而race语义上相当于once,有个执行完成后就会执行then。一定要注意,它们是并发的,只是结果处理的点不一样而已。

它们的使用方法是一样,接收一个promise对象数组为参数。

all.js

  1. 'use strict'
  2. let sleep = (time, info) => {
  3. return new Promise(function (resolve) {
  4. setTimeout(function () {
  5. console.log(info)
  6. resolve('this is ' + info)
  7. }, time)
  8. })
  9. }
  10. let loser = sleep(1000, 'loser')
  11. let winner = sleep(4, 'winner')
  12. // main
  13. Promise.all([winner, loser]).then(value => {
  14. console.log("所有都完成后会执行then,它们是并行的哦: " + value) // => 'this is winner'
  15. })

执行结果

  1. $ node api/all.js
  2. winner
  3. loser
  4. 所有都完成后会执行then,它们是并行的哦: this is winner,this is loser

race.js

  1. 'use strict'
  2. let sleep = (time, info) => {
  3. return new Promise(function (resolve) {
  4. setTimeout(function () {
  5. console.log(info)
  6. resolve('this is ' + info)
  7. }, time)
  8. })
  9. }
  10. let loser = sleep(1000, 'loser')
  11. let winner = sleep(4, 'winner')
  12. // main
  13. Promise.race([winner, loser]).then(value => {
  14. console.log("只要有一个成功,就会执行then,和顺序无关,只看执行速度: " + value) // => 'this is winner'
  15. })

执行结果

  1. $ node api/race.js
  2. winner
  3. 只要有一个成功,就会执行then,和顺序无关,只看执行速度: this is winner
  4. loser

参考阅读

  1. Promises/A
  2. Promises/B
  3. Promises/D
  4. Promisejs
  5. Promises/A+
  6. As soon as possible
  7. A minimalist implementation of a javascript promise
  8. Lightweight implementation of promises
  9. How is a promise/defer library implemented?
  10. Basic Javascript promise implementation attempt
  11. You’re Missing the Point of Promises
  12. Boom! Promises/A+ Was Born
  13. Futures and promises
  14. JavaScript Promises - There and back again
  15. Promise 迷你书
  16. https://blog.domenic.me/youre-missing-the-point-of-promises/
  17. https://strongloop.com/strongblog/promises-in-node-js-with-q-an-alternative-to-callbacks/
  18. https://github.com/kriskowal/q/wiki/General-Promise-Resources
  19. https://www.w3.org/2001/tag/doc/promises-guide
  20. https://github.com/bevacqua/promisees

源码 https://github.com/calvinmetcalf/lie/blob/master/lib/index.js

生成器Generators/yield

Generator Function(生成器函数)和Generator(生成器)是ES6引入的新特性,该特性早就出现在了Python、C#等其他语言中。生成器本质上是一种特殊的迭代器https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/The_Iterator_protocol

Generator函数本意是iterator生成器,函数运行到yield时退出,并保留上下文,在下次进入时可以继续运行。

生成器函数也是一种函数,语法上仅比普通function多了个星号 ,即function ,在其函数体内部可以使用yield和yield* 关键字。

简单理解,这是ES6的新特性,function 后面带 * 的叫做Generator,如以下代码所示。

  1. function* doSomething() {
  2. ....
  3. }

先看一下Generator如何执行:

  1. function* doSomething() {
  2. console.log('1');
  3. yield; // Line (A)
  4. console.log('2');
  5. }
  6. var gen1 = doSomething();
  7. gen1.next(); // Prints 1 then pauses at line (A)
  8. gen1.next(); // resumes execution at line (A), then prints 2

说明

  • gen1是产生出来的Generator对象
  • 第一个next,会打印出1,之后悬停在 yield所在行,即Line (A)
  • 第二个next,恢复line (A)点的执行,之后打印出2

如果有多个yield呢?那么会有无穷无尽的next。

于是tj就写co这个著名的Generator执行器,co目前已经是v4了,彻底的面向Promise了,个中曲折也是够八卦的了。

Async函数/Await(以前说是ES7 stage-3)

Generator的弊病是没有执行器,它本身就不是为流程控制而生的,所以co的出现只是解决了这个问题。

可是,你不觉得奇怪么?为什么非要加个co,才能好好的玩耍?为什么不能是直接就可以执行,并且效果和Yieldable一样的呢?

Async/Await 就是这样被搞出来的,很多人认为它是异步操作的终极解决方案。

Await

Await的3种可能情况

  • Await + Async函数
  • Await + Promise
  • await + co(co会返回Promise,这样可以Yieldable,但难度较大,适合老手)

头2种是比较常用的,第三种co作为promise生成器,是一种hack的办法。

下面给出第一种和第二种的示例:

  1. async function a2() {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(resolve, 1000);
  4. })
  5. }
  6. async function a1() {
  7. console.log("hello a1 and start a2");
  8. await a2();
  9. console.log("hello end a2");
  10. }
  11. async function a0() {
  12. console.log("hello a0 and start a1");
  13. await a1();
  14. console.log("hello end a1");
  15. }
  16. a0()

执行

需要使用babel或者其他支持async函数的编译工具,这里使用runkoa(是为koa支持async函数做的基于babel的简单封装)

  1. $ runkoa async.js
  2. async.js
  3. 3babel presets path = /Users/sang/.nvm/versions/node/v4.4.5/lib/node_modules/runkoa/node_modules/
  4. hello a0 and start a1
  5. hello a1 and start a2
  6. hello end a2
  7. hello end a1

异常处理

Node.js里关于异常处理有一个约定,即同步代码采用try/catch,非同步代码采用error-first方式。对于Async函数俩说,它的Await语句是同步执行的,所以最正常的流程处理是采用try/catch语句捕获,和generator/yield是一样的。

下面的代码所展示的是通用性的做法:

  1. try {
  2. console.log(await asyncFn());
  3. } catch (err) {
  4. console.error(err);
  5. }

很多时候,我们需要把异常做得粒度更细致一些,这时只要把Promise的异常处理好就好了。

Promise里有2种处理异常的方法

  • then(onFulfilled, onRejected)里的onRejected,处理当前Promise里的异常
  • catch处理全局异常

Async函数总结

  • Async函数语义上非常好
  • Async不需要执行器,它本身具备执行能力,不像Generator
  • Async函数的异常处理采用try/catch和Promise的错误处理,非常强大
  • Await接Promise,Promise自身就足够应对所有流程了
  • Await释放Promise的组合能力,外加Promise的then,基本无敌

co引出的“血案”

ES6的Generator本意是为了计算而设计的迭代器,但tj觉得它可以用于流程控制,于是就有了co,co的历史可以说经历了目前所有的流程控制方案,而且由于支持Generator和yield就导致yieldable。

实际上co和Generator是把双刃剑,给了我们强大便利的同时,也增加了非常多的概念,可能是过渡性的,也可能是过时的。

可是,你真的需要了解这么多么?从学习的角度,当然是多多意义,如果从实用的角度看,你可能不需要。

存在即合理,那么我们就看看这“血案”吧:

  • 学习ES6的Generator
  • 了解ES6的迭代器和迭代器相关的2种协议,了解for-of
  • 了解co和co的2种用法,源码
  • 了解yieldable 5种(包括不常用Thunk)
  • 如果是koa,还需要了解convert和compose

5种 yieldable

yieldable本来是没有这个词的,因为在Generator里可以是yield关键词,而yield后面接的有5种可能,故而把这些可以yield接的方式成为yieldable,即可以yield接的。

  • Promises
  • Thunks (functions)
  • array (parallel execution)
  • objects (parallel execution)
  • Generators and GeneratorFunctions

这里我把co和promise做了简单的关键,同时区分Yieldable里的并行和顺序执行处理方式,以便大家能够更好的理解co和Yieldable。

Co

  • 顺序执行
    • Promises
    • Thunks
  • 并行
    • array
    • objects

无论是哪种,它们其实都可以是Promise,而既然是Thunk对象,它们就可以thenable,而co v4.6版本的执行的返回值就是Promise,至此完成了左侧闭环。

至于Generator和GeneratorFunction就要从yield和yield*讲起,在koa 1.x和2.x里有明显的应用。

最关键的是,Generator是用来计算的迭代器,它是过渡性的产物。yiedable足够强大,只是学习成本稍高,理解起来也有些难度。

推导出学习重点

综上所述

  • Async函数是趋势,如果Chrome 52. v8 5.1已经支持Async函数(https://github.com/nodejs/CTC/issues/7)了,Node.js支持还会远么?
  • Async和Generator函数里都支持promise,所以promise是必须会的。
  • Generator和yield异常强大,不过不会成为主流,所以学会基本用法和promise就好了,没必要所有的都必须会。
  • co作为Generator执行器是不错的,它更好的是当做Promise 包装器,通过Generator支持yieldable,最后返回Promise,是不是有点无耻?

我整理了一张图,更直观一些。

All

  • 红色代表Promise,是使用最多的,无论async还是generator都可用
  • 蓝色是Generator,过度货
  • 绿色是Async函数,趋势

结论:Promise是必须会的,那你为什么不顺势而为呢?

推荐:使用Async函数 + Promise组合,如下图所示。

Suggest

实践

合理的结合Promise和Async函数是可以非常高效的,但也要因场景而异

  • Promise更容易做promisefyAll(比如使用bluebird)
  • Async函数无法批量操作

那么,在常见的Web应用里,我们总结的实践是,dao层使用Promise比较好,而service层,使用Async/Await更好。

dao层使用Promise:

  • crud
  • 单一模型的方法多
  • 库自身支持Promise

这种用promisefyAll基本几行代码就够了,一般单一模型的操作,不会特别复杂,应变的需求基本不大。

而service层一般是多个Model组合操作,多模型操作就可以拆分成多个小的操作,然后使用Await来组合,看起来会更加清晰,另外对需求应变也是非常容易的。