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
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 ()
// 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 是更好的选择。