async/await 是 es2017 的重要新特性。async/await 和 es2015 发布的 generators 有很多相似之处。在 stackoverflow 有很多关于这两者不同之处的提问,其中也有一些不错的回答。
如果你用过 co 模块,基于 generator 的代码看起来会很像 async/await。

以下是 async/await 处理 HTTP 请求三次。

async function test() {
  let i
  for (i = 0; i < 3; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error')
      break
    } catch (err) {}
  }
  console.log(i) // 3
}

相同功能的 generator 实现:

const test = co.wrap(function*() {
  let i
  for (i = 0; i < 3; ++i) {
    try {
      yield superagent.get('http://bad.domain')
      break
    } catch (err) {}
  }
  console.log(i) // 3
})

通过观察,你可以写一个将 async/await 转换成 generators 的转换器,原理就是将 async function() {}替换成 co.wrap(function*() {}),将 await 替换为 yield。 所以这两者到底有什么不同?

不同点

很重要的一点不同是 generators 在 Node.js 4.x 就开始支持,而 async/await 要求 Node.js >= 7.6.0。不过 Node.js 4.x 早就不再维护,Node.js 6.x 也在 2019 年终止维护, 所以这个不同点现在没那么重要了。

另一点不同是 co 模块是开发者维护的第三方模块,而 async/await 是 js 语言的一部分。所以你需要将 co 写到 package.json 里,而 async/await 则不需要,不过如果你想支持老旧的浏览器,你就需要配置一下转换器。

stack traces 得到的错误不同。async/await 得到的错误比 generators 要清晰。而且,由于 async/await 是 JavaScript 语言的核心部分,而不是像 co 这样的用户级库,因此将来可能会对 async/await 堆栈跟踪进行更多改进。

这里有个例子展示 async 函数抛出的错误。

async function runAsync() {
  await new Promise(resolve => setTimeout(() => resolve(), 100))
  throw new Error('Oops!')
}

// Error: Oops!
//    at runAsync (/home/val/test.js:5:9)
//    at <anonymous>
runAsync().catch(error => console.error(error.stack))

以下是用 generators 实现的相同功能,注意错误里出现的 onFulfilled()Generator.next() 透漏了 co 模块是怎么工作的

const co = require('co')

const runCo = co.wrap(function*() {
  yield new Promise(resolve => setTimeout(() => resolve(), 100))
  throw new Error('Oops!')
})

// Error: Oops!
//     at D:\code\js\test\babel-test\src\co_test.js:5:9
//     at Generator.next (<anonymous>)
//     at onFulfilled (D:\code\js\test\babel-test\node_modules\co\index.js:65:19)
runCo().catch(error => console.error(error.stack))

Thunks 和 Promise 转换

async/await 仅仅用于 Promise, 如果用于非 Promise 是没有用的。

async function runAsync() {
  // res 将会是一个 function
  // 因为 function 不是 promise,所以括号是语法所必需的
  const res = await (cb => cb(null, 'test'))
  console.log(res)
}

runAsync().catch(error => console.error(error.stack))

另一方来看,co 将 yield 的值转成 Promise。当你 yield 带有单个参数的函数,即 Node.js 样式的回调,co 会把它转成 promise。

const co = require('co')

const runCo = co.wrap(function*() {
  // `res` will be a string, because co converts the
  // value you `yield` into a promise. The `yield cb => {}`
  // pattern is called a _thunk_.
  const res = yield cb => cb(null, 'test')
  console.log(res)
})

runCo().catch(error => console.error(error.stack))

同样,co 也可以起到 Promise.all() 相似的效果。

async function runAsync() {
  // 用 co 的话,你可以写
  // `yield [Promise.resolve('v1'), Promise.resolve('v2')]`
  const res = await Promise.all([
    Promise.resolve('v1'),
    Promise.resolve('v2');
  ]);
  // 'v1 v2'
  console.log(res[0], res[1]);
}

第三方库的好处

在许多时候,generators 是 async/await 的超集。用 generators,你可以使用它的一些强大特性转换成你自己的 async/await. Co 内置的 Promise 转换只是冰山一角。举个例子,我曾经建立了一个类似 co 的库,该库返回了一个可观察的对象。使用 RxJS 的 filter 运算符,处理错误将非常容易。

const corx = require('./')
require('rxjs/add/operator/filter')

corx(function*() {
  yield Promise.resolve('Test 1')
  try {
    yield Promise.reject('Test 2')
  } catch (error) {}
  console.log('Reached the end')
})
  .filter(v => !!v)
  .subscribe(
    op$ => {
      // This will print, even though the error was caught, because
      // this callback executes on every single async operation!
      op$.subscribe(
        () => {},
        err => console.log('Error occurred', err)
      )
    },
    error => console.log('This will not print'),
    () => console.log('Done')
  )

上面的杀手级功能是,当 subscribe() 时,generator 函数中发生的每个异步操作都会获得一个回调。
这意味着您可以在不实际更改任何逻辑的情况下,通过 debugging, profiling, error handling 来检测每个单独的异步操作!

这个特性很酷,但是还不足以让我们抛弃 async/await 用 generator。async/await 的优点在于它在大部分时间都满足您的需求,而这个基于 observable 的库实际上将为您解决什么问题?
为了使调试起作用,您将需要一种从可观察的 op$ 中提取有意义的信息的方法,在一般情况下,除了这种我从来没有找到其他方法。

这就是为什么我要重新看重middleware,将其作为解决跨领域问题的正确工具。

另外,对于 async/await 的适用情况,可观察对象可能并不是很好的选择,因为它们会解释为多个值,甚至可能是无限值的循环。

总结

async/await 和 generators 乍一看很相似,但两者之间存在许多有意义的差异。

async/await 不需要第三方库,看起来更简洁;generators 通常要配合第三方库使用,但是这些第三方库又提供了很多 async/await 不具备的强大的功能。换句话说,async/await 和 generators 之间的权衡就是简洁与灵活之间的权衡。

作为高级开发人员,您可以在某些情况下从开发人员那里获得有意义的价值,但是大多数情况下,async/await 是更好的选择。


myths
149 声望2 粉丝

用你的脑子去创造一个属于自己的未来