一直以来,“异步”编程问题一直困扰着广大的 JavaScript 开发者。近年来出现了各种异步解决方案,从基于最原始的
callback
方式的
async
函数,到
promise
标准,再到基于
generator
的
co
库,以及即将纳入 ES7 标准的
async function / await
语法,但是由于各种现实的原因,它们的表现并不尽人意。
原始的
callback
方式简单明了,不需要过多的依赖,但是在异步逻辑较复杂的场景下写出来的程序并不太直观,就我个人的使用经验而言,尽管多年来已经练就了一身可以穿梭在各种嵌套回调的“乱码”之中,每次重新看这些代码都头疼不已。
JavaScript 异步解决方案都是朝着更直观(跟写同步代码一样)的方向发展的,比如近来呼声最高的
async function / await
语法,直接从语言层面解决问题,使用体验那是好得没法说的。但是,这是一个 ES7 (ES2017,即明年才会发布的 ES 标准)标准的语法,目前并没有得到各 JavaScript 引擎的内置支持。虽然我们照样可以使用 Babel 神器来将它编译成 ES5 / ES6 的语法,然后运行在现有的 JavaScript 引擎之上。然而使用 Babel 编译后的代码并不易于维护,首先这些代码修改后要先经过一次编译,当我们在生产环境上执行编译后的代码时,很难准确地定位到源码出错的位置。另外,根据最新可靠的消息,Node v7 版本会在语法层面上支持
async function / await
语法,但该版本原计划于 9 月 30 号发布却跳票了,而且按照往年的惯例,也要在 1 年后发布的 Node v8 LTS 版本上才会正式支持该语法,这对于追求稳定的企业来说还需要一个漫长的等待过程。
通过 Babel 编译 async function / await 语法解决方案
利用
async function / await
语法,我们可以很直观地书写异步程序:
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
async function test() {
for (let i = 0; i < 100; i++) {
await sleep(100);
但由于目前的 JavaScript 引擎均不支持该语法,需要通过 Babel 之类的工具编译成 ES6 语法后的程序是这样的:
"use strict";
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { return step("next", value); }, function (err) { return step("throw", err); }); } } return step("next"); }); }; }
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
let test = function () {
var ref = _asyncToGenerator(function* () {
for (let i = 0; i < 100; i++) {
yield sleep(100);
return function test() {
return ref.apply(this, arguments);
从编译后的代码来看,虽然在前面增加了一坨_asyncToGenerator
函数的代码,但test
函数的代码还是能看出程序原来的结构。通过以上凌乱的代码我们还是可以看出,其内部是通过generator function
实现的,在外层返回一个promise
对象。
基于 generator 与 promise 的解决方案
首先我们要达成这样的共识:async function / await
语法是未来的主流,但是要让主流的 JavaScript 引擎支持该语法还需要一个很漫长的过程,而现在我们需要找到一种替代的方法,而这种方法又能尽量保持与async function / await
非常相近,在以后可以很轻易地替换成新的用法。 基于以上的考虑可以得出以下结论:
异步函数执行后需要返回一个promise
对象(async function
执行后返回的也是一个promise
对象)
使用generator function
,yield
代替await
,这样可以最大程度上保持程序逻辑结构不变(generator function
从 Node v4 已经开始支持,经过两年多的使用验证,性能和可靠性性上还是有保证的)
基于以上两点的考虑,我们可以假设新的异步代码应该是这样的:
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
const test = coroutine(function* () {
for (let i = 0; i < 100; i++) {
yield sleep(100);
console.log('i=%s', i);
return 100;
test()
.then(i => console.log('执行了%s 次 sleep', i))
.catch(err => console.error('出错', err));
对比直接使用async function / await
语法,我们发现只是在声明异步函数和yield
这两行写法不同,它可以在 Node v4 及更高版本上可直接执行,并且可以直接在源码上进行调试。以下是上文的程序在 Visual Studio Code 上进行调试的界面(coroutine
函数的实现将在下文讲解):
实现一个简单的 coroutine 函数
本小节只是是为了通过演示如何动手写一个coroutine
函数来了解其中的原理,实际久经考验的bluebird
模块和co
模块已经实现了此功能,下一小节将会讲解基于这些现成模块的使用方法。
首先我们需要了解一下 Generator 的概念。Generator 中文名称为“生成器”,通过function*
来定义的函数称之为“生成器函数”(generator function
),而生成器函数执行后返回的是一个生成器对象(Generator
),这个生成器对象包含了几个方法,其中一个重要的方法是next()
,我们可以通过不断地调用next()
来取得在生成器中yield
出来的值,生成器是否已执行结束则可以通过返回值的done
属性来判断。
生成器有一个特点就是它可以中断函数的执行,每次执行yield
语句之后,函数即暂停执行,直到调用返回的生成器对象的next()
函数它才会继续执行。以下是一个简单的例子:
'use strict';
function* genNumbers(n) {
for (let i = 1; i <= n; i++) {
yield i;
return 'ok';
const gen = genNumbers(10);
while (true) {
const ret = gen.next();
console.log(ret);
if (ret.done) {
break;
console.log('done');
上面的代码执行后的结果如下:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: 5, done: false }
{ value: 6, done: false }
{ value: 7, done: false }
{ value: 8, done: false }
{ value: 9, done: false }
{ value: 10, done: false }
{ value: 'ok', done: true }
每次执行next()
都会返回一个包含{ value, done }
两个属性的对象,其中value
是该次yield
返回的值,done
表示是否执行结束
最后一次返回的值是生成器函数内return
语句返回的值
从上文的代码可知,只有我们执行gen.next()
时生成器才会继续执行。如果还不太确定,我们可以尝试把它换成异步的执行方式:
'use strict';
function* genNumbers(n) {
for (let i = 1; i <= n; i++) {
yield i;
return 'ok';
const gen = genNumbers(10);
function next() {
const ret = gen.next();
console.log(ret);
if (ret.done) {
console.log('done');
} else {
setTimeout(next, 500);
next();
如无意外,执行上面的代码后我们应该能看到每隔 0.5 秒会打印出一行结果,直到 5 秒后程序才执行结束,而打印的结果跟之前的一模一样。
现在我们不妨假设,在我们的异步函数中,通过yield
返回一个promise
对象,然后等待promise
执行回调后再执行gen.next()
方法,如此循环,是不是就可以实现异步流程控制呢?
const ret = gen.next();
if (ret.done) {
resolve(ret.value);
} else {
ret.value
.then(() => ret.next())
.catch(err => reject(err));
以下是这个简单coroutine
函数的代码:
'use strict';
function isPromise(p) {
return p && typeof p.then === 'function' && typeof p.catch === 'function';
function coroutine(genFn) {
return function () {
return new Promise((resolve, reject) => {
const gen = genFn.apply(null, arguments);
let ret;
function next(value) {
ret = gen.next(value);
if (ret.done) {
return resolve(ret.value);
if (!isPromise(ret.value)) {
return reject(new TypeError('You may only yield a promise, but the following object was passed: ' + String(ret.value)));
ret.value.then(next).catch(reject);
next();
说明:此代码仅用作演示,尽管通常情况下它也能正确地运行,但是并没有考虑性能问题和一些异常情况,生产环境下请使用稳定的 NPM 模块。
使用 bluebird 模块的 coroutine 函数
使用前先执行以下命令安装bluebird
模块:
npm install bluebird --save
以下是基于bluebird
模块的coroutine
函数的使用方法:
'use strict';
const Promise = require('bluebird');
const test = Promise.coroutine(function* (n, ms) {
for (let i = 0; i < n; i++) {
console.log('i=%s', i);
yield Promise.delay(ms);
return n;
test(10, 500)
.then(n => console.log('执行结束,n=%s', n))
.catch(err => console.error('执行出错:', err));
bluebird
自带了delay()
函数,功能与上文实现的sleep()
相同
使用 co 模块
使用前先执行以下命令安装co
模块:
npm install co --save
以下是基于co
模块的简单使用方法:
'use strict';
const co = require('co');
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
const test = co.wrap(function* (n, ms) {
for (let i = 0; i < n; i++) {
console.log('i=%s', i);
yield sleep(ms);
return n;
test(10, 500)
.then(n => console.log('执行结束,n=%s', n))
.catch(err => console.error('执行出错:', err));
实际上bluebird
模块和co
模块还是有区别的:bluebird
模块只支持yield
一个promise
对象,而co
模块可以支持promise
,generator
,array
,object
和Thunk
函数,可在异步函数内实现多个并发异步任务,比前者复杂得多。
回想在过去的一年多时间里,我确实是对以使用generator
的co
模块来解决异步问题是有些许偏见,也曾喷过某月饼云的 Node.js SDK 竟然不支持callback
而是直接返回一个generator
。究其原因,我深以为有以下几点:
早期版本的co
封装并不是返回一个promise
对象,再加上大多数介绍co
的文章讲的基本上都是thunks
的概念,这对初使用co
的人是相当恶心的
co
的yield
支持的功能实在太丰 fu 富 za 了,而我更喜欢简单的
在 Node v4 发布之前,使用 Generator 还需要开启 Harmony 特性
从 Node v4 开始,直接支持了 Generator 和 Promise
最后一句,JavaScript 的世界变化实在太快了。
Generator 函数的含义与用法
你不懂 JS: 异步与性能 第四章: Generator(上)
你不懂 JS: 异步与性能 第四章: Generator(下)
co 函数库的含义和用法
Koa, co and coroutine
生成器(Generator)——《实战 ES2015》章节试读