Nodejs 的 co 库与原生 async/await 的对比

Author Avatar
Equim 2017年2月23日
  • 在其它设备中阅读本文章
Read: 8 minWords: 1,788Last Updated: 17-12-25Written in: MarkdownLicense: CC-BY-NC-4.0

co(意为 coroutine)是一个非常精简的异步控制流工具,通过它可以使用 generator 将异步回调写法改为同步写法。
async/await 是来自ES7新规范的关键字,参考了C#等其他语言的解决方案。

在 v7.6.0 之前,使用 async/await 需要加上--harmony-async-await这个 flag。
目前 Nodejs v7.6.0 已正式支持 async/await,不需要--harmony-async-await了!

回调函数的过度嵌套增加了代码的复杂程度,更为严重的是,回调函数剥夺了我们使用returntry-catch-throw的权利。对此,已有 Promise, EventEmitter, Timers, Streams 等多种替代方案,其中目前接受度较高的是 Promise,Nodejs 也已原生支持 Promise。本文假定你已对 Promise 已经有了一定的了解。

首先来看一个简单的例子,使用 co 的写法如下

'use strict';

const co = require('co');
const program = require('commander');
const chalk = require('chalk');

program
  .option('-e, --test-exception')
  .parse(process.argv);

// 记录最后一次输出的时间
let last;

// 这里的 wait 函数返回一个 thunk,这是 co 的传统用法
// 即它返回一个函数 Fn、且 Fn 的参数为一个回调函数 callback、且这个回调函数 callback 的第一个参数为错误、第二个参数为结果
// 对于 co 来说,使用 Promise 也是可以的,即下文 async/await 版本中wait 函数的写法
const wait = (ms) => (callback) => setTimeout(callback, ms);

const logging = (log) => {
  const now = Date.now();
  console.log(`+${ Math.round((now - last) / 1000) }s ${ chalk.bold(log) }`);
  last = now;
};

co(function* () {
  last = Date.now();
  logging('两秒后开始');

  yield wait(2000);

    // 一个 Promise,定义后立即执行
  const fn = co(function* () {
    logging(chalk.yellow('Promise已开始异步执行(单线程),在此期间可以做别的事'));

    yield wait(4000);
    logging(chalk.yellow('Promise异步执行中(已进行4秒)...'));

    yield wait(2000);
    if (program.testException) {
            // 异常测试
      logging(chalk.yellow('Promise抛出了一个异常,准备把Error本身作为返回值返回'));
      throw new Error('吔屎啦');
    }

    yield wait(4000);
    logging(chalk.yellow('Promise准备返回(已进行10秒)'));

    return 'Equim';
  }).catch(err => err);       // 实际上这里是返回抛出的 Error,然后再在外面判断类型。
                              // 如果需要在 Promise 内立即对 Error 进行处理,则应该在内部就包含 try catch

  yield wait(10);
  logging(chalk.cyan('开始做别的事情...'));

  yield wait(3000);
  logging(chalk.cyan('别的事情执行中(已进行3秒)...'));

  yield wait(5000);
  logging(chalk.cyan('别的事情已经做完了(已进行8秒),正在等待Promise完成(可能得到返回值或者异常)...'));

  const result = yield fn;
  if (result instanceof Error) {
    logging(chalk.cyan('结果:Promise在执行途中抛出了异常'));
    logging(chalk.red(result.stack.red));
  } else {
    logging(chalk.cyan(`结果:Promise的返回值为${ result }`.));
  }
});

测试一下

再测试一下异常的情况

co 中”yieldable”的对象有这些:

  • promises
  • thunks (functions)
  • array (parallel execution)
  • objects (parallel execution)
  • generators (delegation)
  • generator functions (delegation)

在旧版本的 co 中,co()返回的是一个 thunk;在4.0.0版本之后,co()返回的是原生的 Promise。
调用co()会立即执行其中的内容并返回一个 Promise。co 还提供了一个 wrap 方法,利用co.wrap()可以将 generator 封装为一个返回 Promise 的函数。

const task = co(function* () {
  yield Promise.resolve(2333);
});

console.log(task instanceof Promise);   //=> true
const fn = co.wrap(function* () {
  yield Promise.resolve(2333);
});

console.log(typeof fn);     //=> function

const task = fn();

console.log(task instanceof Promise);   //=> true

yield后面接的 Promise 也可以是第三方的 Promise,但是co()co.wrap()()返回的仍是 Node 原生的 Promise。

const Bluebird = require('bluebird');
const co = require('co');

const wait = (ms) => new Bluebird((resolve, reject) => setTimeout(resolve, ms));

const fn = co.wrap(function* (arg) {
  console.log('started');

  const waitPromise = wait(5000);
  console.log(waitPromise instanceof Bluebird);    //=> true
  console.log(waitPromise instanceof Promise);     //=> false

  yield waitPromise;

  return arg * 2;
});

co(function* () {
  const task = fn(2333);

  yield wait(1000);
  console.log(task instanceof Bluebird);      //=> false
  console.log(task instanceof Promise);       //=> true

  console.log(`The promise is resolved with ${ yield task }`);  //=> "... 4666"
});

如果要在 co 中更完美地使用其他第三方的 Promise 库(如 bluebird 等),可以用 co-use 这个包。

const Bluebird = require('bluebird');
const co = require('co-use').use(Bluebird);

const wait = (ms) => new Bluebird((resolve, reject) => setTimeout(resolve, ms));

const fn = co.wrap(function* (arg) {
  console.log('started');
  yield wait(5000);
  return arg * 2;
});

co(function* () {
  const task = fn(2333);

  console.log(task instanceof Bluebird);      //=> true
  console.log(task instanceof Promise);       //=> false

  console.log(`The promise is resolved with ${ yield task }`);  //=> "... 4666"
});

实际上 bluebird 也提供了Promise.coroutine这个方法来实现类似co.wrap的功能,不过直接使用的话依然只能 yield Promise。若要像 co 那样在 yield 后接别的对象,则需要利用Promise.coroutine.addYieldHandler进行转换。具体可以参照相应文档


接着我们来看看 async/await 的对应写法

'use strict';

const program = require('commander');
const chalk = require('chalk');

program
  .option('-e, --test-exception')
  .parse(process.argv);

// 记录最后一次输出的时间
let last;

// 这里的 wait 函数返回一个 Promise
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

const logging = (log) => {
  const now = Date.now();
  console.log(`+${ Math.round((now - last) / 1000) }s ${ chalk.bold(log) }`);
  last = now;
};

(async () => {
  last = Date.now();
  logging('两秒后开始');

  await wait(2000);

    // 一个 Promise,定义后立即执行
  const fn = (async () => {
    logging(chalk.yellow('Promise已开始异步执行(单线程),在此期间可以做别的事'));

    await wait(4000);
    logging(chalk.yellow('Promise异步执行中(已进行4秒)...'));

    await wait(2000);
    if (program.testException) {
            // 异常测试
      logging(chalk.yellow('Promise抛出了一个异常,准备把Error本身作为返回值返回'));
      throw new Error('吔屎啦');
    }

    await wait(4000);
    logging(chalk.yellow('Promise准备返回(已进行10秒)'));

    return 'Equim';
  })().catch(err => err);

  await wait(10);
  logging(chalk.cyan('开始做别的事情...'));

  await wait(3000);
  logging(chalk.cyan('别的事情执行中(已进行3秒)...'));

  await wait(5000);
  logging(chalk.cyan('别的事情已经做完了(已进行8秒),正在等待Promise完成(可能得到返回值或者异常)...'));

  let result = await fn;
  if (result instanceof Error) {
    logging(chalk.cyan('结果:Promise在执行途中抛出了异常'));
    logging(chalk.red(result.stack));
  } else {
    logging(chalk.cyan(`结果:Promise的返回值为${ result }`));
  }
})();

测试一下

再测试一下异常的情况

可以看出,两者在语法上极为类似,不过await后面只能跟 Promise。和 co 类似,虽然await后面也可以接第三方的 Promise,但是调用 async 函数的返回值仍是原生的 Promise。

const Bluebird = require('bluebird');

const wait = (ms) => new Bluebird((resolve, reject) => setTimeout(resolve, ms));

const fn = async (arg) => {
  console.log('started');

  const waitPromise = wait(5000);
  console.log(waitPromise instanceof Bluebird);    //=> true
  console.log(waitPromise instanceof Promise);     //=> false

  await waitPromise;

  return arg * 2;
};

(async () => {
  const task = fn(2333);

  await wait(1000);
  console.log(task instanceof Bluebird);      //=> false
  console.log(task instanceof Promise);       //=> true

  console.log(`The promise is resolved with ${ await task }`);  //=> "... 4666"
})();

要将 async 函数返回的原生 Promise 给 wrap 成 bluebird 的 Promise 的话,目前我知道的方法只有使用 bluebird 提供的Promise.resolve。在上例中,可以这样

// ...
(async () => {
  const task = Bluebird.resolve(fn(2333));

  await wait(1000);
  console.log(task instanceof Bluebird);      //=> true
  console.log(task instanceof Promise);       //=> false

  console.log(`The promise is resolved with ${ await task }`);  //=> "... 4666"
})();

就目前而言

  • 使用 co 的理由:
    • co 中还可以yield 数组、thunk 等,而 async/await 只能 await Promise
    • 配合 co-use 还可以拓展使用第三方的 Promise 库,而 async 函数还不能(Node 原生的 Promise 目前还存在一些问题,后面的文章会提到)
    • 兼容性较好
  • 使用 async/await 的理由:
    • 原生支持,能减少依赖,语义上也更加规范。co的手段还是hack了一点,毕竟 generator 的本意也不在于此
    • 总的来说 async/await 使用起来比 co 要简洁

知识共享许可协议
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。

本文链接:https://ekyu.moe/article/compare-async-await-and-co-in-js/