添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
谦和的墨镜  ·  JavaScript ...·  昨天    · 
月球上的眼镜  ·  Look Dev | High ...·  2 月前    · 
越狱的大脸猫  ·  How do I extract a ...·  5 月前    · 
怕老婆的海豚  ·  网站正在建设中.·  7 月前    · 
发怒的花卷  ·  中国科普博览·  7 月前    · 

一直以来,“异步”编程问题一直困扰着广大的 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 语法,我们可以很直观地书写异步程序:

// sleep 函数,返回一个 Promise 对象
function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
async function test() {
  // 循环 100 次
  for (let i = 0; i < 100; i++) {
    // 等待 100ms 再返回
    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 functionyield代替await,这样可以最大程度上保持程序逻辑结构不变(generator function从 Node v4 已经开始支持,经过两年多的使用验证,性能和可靠性性上还是有保证的)
  • 基于以上两点的考虑,我们可以假设新的异步代码应该是这样的:

    // sleep 函数,返回一个 Promise 对象
    function sleep(ms) {
      return new Promise((resolve, reject) => {
        setTimeout(resolve, ms);
    // 通过 coroutine 来包装异步函数
    const test = coroutine(function* () {
      // 循环 100 次
      for (let i = 0; i < 100; i++) {
        // 等待 100ms 再返回
        yield sleep(100);
        console.log('i=%s', i);
      // 返回执行 sleep 次数
      return 100;
     // 执行函数,其返回一个 Promise 对象
    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) {
      // 执行 next()方法取下一个数字
      const ret = gen.next();
      // 打印结果
      console.log(ret);
      if (ret.done) {
        // 如果 done=true 则表示生成器执行结束
        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() {
      // 执行 next()方法取下一个数字
      const ret = gen.next();
      // 打印结果
      console.log(ret);
      if (ret.done) {
        // 如果 done=true 则表示生成器执行结束
        console.log('done');
      } else {
        // 500ms 后继续执行
        setTimeout(next, 500);
    next();
    

    如无意外,执行上面的代码后我们应该能看到每隔 0.5 秒会打印出一行结果,直到 5 秒后程序才执行结束,而打印的结果跟之前的一模一样。

    现在我们不妨假设,在我们的异步函数中,通过yield返回一个promise对象,然后等待promise执行回调后再执行gen.next()方法,如此循环,是不是就可以实现异步流程控制呢?

    const ret = gen.next();
    if (ret.done) {
      // 执行结束
      resolve(ret.value);
    } else {
      // 等待 promise 回调
      ret.value
        .then(() => ret.next())
        .catch(err => reject(err));
    

    以下是这个简单coroutine函数的代码:

    'use strict';
    // 判断是否为 Promise 对象,再次只简单判断该对象是否包含 then 和 catch 方法
    function isPromise(p) {
      return p && typeof p.then === 'function' && typeof p.catch === 'function';
    // coroutine 函数,接收一个 generator function 作为参数,返回一个新的函数
    function coroutine(genFn) {
      return function () {
        // 函数执行结果是一个 promise 对象
        return new Promise((resolve, reject) => {
          // 首先执行 generator function,它会返回一个 Generator 对象
          const gen = genFn.apply(null, arguments);
          let ret;
          function next(value) {
            // 执行.next()返回 yield 返回的值
            // next()可以接收一个参数,用作在生成器函数里面 yield 语句的返回值
            ret = gen.next(value);
            // 如果 done=true 则表示结束
            if (ret.done) {
              return resolve(ret.value);
            // 如果返回的值不是 promise 则报错
            if (!isPromise(ret.value)) {
              return reject(new TypeError('You may only yield a promise, but the following object was passed: ' + String(ret.value)));
            // 等待 promise 执行结果
            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模块可以支持promisegeneratorarrayobjectThunk函数,可在异步函数内实现多个并发异步任务,比前者复杂得多。

    回想在过去的一年多时间里,我确实是对以使用generatorco模块来解决异步问题是有些许偏见,也曾喷过某月饼云的 Node.js SDK 竟然不支持callback而是直接返回一个generator。究其原因,我深以为有以下几点:

  • 早期版本的co封装并不是返回一个promise对象,再加上大多数介绍co的文章讲的基本上都是thunks的概念,这对初使用co的人是相当恶心的
  • coyield支持的功能实在太丰 fu 富 za 了,而我更喜欢简单的
  • 在 Node v4 发布之前,使用 Generator 还需要开启 Harmony 特性
  • 从 Node v4 开始,直接支持了 Generator 和 Promise
  • 最后一句,JavaScript 的世界变化实在太快了。

  • Generator 函数的含义与用法
  • 你不懂 JS: 异步与性能 第四章: Generator(上)
  • 你不懂 JS: 异步与性能 第四章: Generator(下)
  • co 函数库的含义和用法
  • Koa, co and coroutine
  • 生成器(Generator)——《实战 ES2015》章节试读
  •