笔者在很长一段时间内都使用 koa@1 +(generator|bluebird)+ sequelize 这个组合,这个组合并没有什么问题,也很常见,但是到了滥用的地步,导致后来维护和调试起来都很痛苦。若排除 sequelize 这个我们不得不用的模块,从调试 cpuprofile 角度讲讲为什么笔者认为应该用 async/await + Promise 替代 co + generator|bluebird。

笔者的观点是:使用原生模块具有更清晰的调用栈

下面用 4 个例子进行对比,看看实现相同逻辑的不同代码生成的 cpuprofile 中调用栈的信息。

3.2.1 async + await

async.js

  1. const fs = require('fs')
  2. const profiler = require('v8-profiler')
  3. async function A () {
  4. return await Promise.resolve('A')
  5. }
  6. async function B () {
  7. return await A()
  8. }
  9. (async function asyncWrap () {
  10. const start = Date.now()
  11. profiler.startProfiling()
  12. while (Date.now() - start < 10000) {
  13. await B()
  14. }
  15. const profile = profiler.stopProfiling()
  16. profile.export()
  17. .pipe(fs.createWriteStream('async.cpuprofile'))
  18. .on('finish', () => {
  19. profile.delete()
  20. console.error('async.cpuprofile export success')
  21. })
  22. })()

加载运行后生成的 async.cpuprofile,如下所示:

Async + Await - 图1

可以看出:asyncWrap 中调用了 B 函数,B 函数调用了 A 函数,A 函数中 resolve 了一个值。在 asyncWrap 中还调用了 stopProfiling 函数。

3.2.2 co + yield

co.js

  1. const fs = require('fs')
  2. const co = require('co')
  3. const profiler = require('v8-profiler')
  4. function * A () {
  5. return yield Promise.resolve('A')
  6. }
  7. function * B () {
  8. return yield A()
  9. }
  10. co(function * coWrap () {
  11. const start = Date.now()
  12. profiler.startProfiling()
  13. while (Date.now() - start < 10000) {
  14. yield B()
  15. }
  16. const profile = profiler.stopProfiling()
  17. profile.export()
  18. .pipe(fs.createWriteStream('co.cpuprofile'))
  19. .on('finish', () => {
  20. profile.delete()
  21. console.error('co.cpuprofile export success')
  22. })
  23. })

加载运行后生成的 co.cpuprofile,如下所示:

Async + Await - 图2

可以看出:调用栈非常深,有太多没有用的 co 相关的调用栈。如果 n 个 generator 层层嵌套,就会出现 n 倍的 (anonymous)->onFullfiled->next->toPromise->co->Promise->(anonymous) 调用栈。如果你读过 co 的源码,就可能知道,这是 co 将 generator 解包的过程。其实这个可以通过将 yield generator 替换成 yield* generator 来优化。

3.2.3 co + yield*

co_better.js

  1. const fs = require('fs')
  2. const co = require('co')
  3. const profiler = require('v8-profiler')
  4. function * A () {
  5. return yield Promise.resolve('A')
  6. }
  7. function * B () {
  8. return yield * A()
  9. }
  10. co(function * coWrap () {
  11. const start = Date.now()
  12. profiler.startProfiling()
  13. while (Date.now() - start < 10000) {
  14. yield * B()
  15. }
  16. const profile = profiler.stopProfiling()
  17. profile.export()
  18. .pipe(fs.createWriteStream('co_better.cpuprofile'))
  19. .on('finish', () => {
  20. profile.delete()
  21. console.error('co_better.cpuprofile export success')
  22. })
  23. })

加载运行后生成的 co_better.cpuprofile,如下所示:

Async + Await - 图3

可以看出:与 co.js 相比,调用栈清晰了很多,不过与使用 async/await 相比,还是多了些 onFulfilled、next。

3.2.4 co + bluebird

co_bluebird.js

  1. const fs = require('fs')
  2. const co = require('co')
  3. const Promise = require('bluebird')
  4. const profiler = require('v8-profiler')
  5. function * A () {
  6. return yield Promise.resolve('A')
  7. }
  8. function * B () {
  9. return yield * A()
  10. }
  11. co(function * coBluebirdWrap () {
  12. const start = Date.now()
  13. profiler.startProfiling()
  14. while (Date.now() - start < 10000) {
  15. yield * B()
  16. }
  17. const profile = profiler.stopProfiling()
  18. profile.export()
  19. .pipe(fs.createWriteStream('co_bluebird.cpuprofile'))
  20. .on('finish', () => {
  21. profile.delete()
  22. console.error('co_bluebird.cpuprofile export success')
  23. })
  24. })

加载运行后生成的 co_bluebird.cpuprofile,如下所示:

Async + Await - 图4

可以看出:与 co_better.js 相比,调用栈中多了许多 bluebird 模块的无用信息。而且这只是非常简单的示例代码,要是在复杂的业务逻辑中大量使用 bluebird 代码生成的 cpuprofile,就几乎没法看了。

结论:使用 async/await + Promise + 命名函数,具有更清晰的调用栈,让分析 cpuprofile 时不再痛苦。

聪明的你可能会问:

  1. 为什么不建议用 bluebird?因为:

    1. 随着 V8 不断优化,原生 Promise 性能逐渐提高,bluebird 的性能优势不明显。
    2. 原生 Promise 的 API 足够用,至少能 cover 大部分使用场景,而且还在不断完善,未来还会添加新的 API,例如:Promise.prototype.finally。
    3. 具有更清晰的调用栈。
  2. 由于历史遗留原因,现在代码中大量使用了 yield + generator 怎么办?可以:

    1. 将所有 yield generator 替换成 yield * generator。
    2. 升级到 node@8+,逐步用 async/await 替换,毕竟 async 函数调用后返回的也是一个 promise,也是 yieldable 的。
  3. 性能比较呢?
    1. node@8+ 下 async/await 完胜 co。

3.2.5 yield -> yield* 遇到的坑

上面讲到,可以将 yield generator 改成 yield * generator,这里面有一个坑,是由于不明白 co 的原理而滥用 co 导致的。代码如下:

  1. const co = require('co')
  2. function * genFunc () {
  3. return Promise.resolve('genFunc')
  4. }
  5. co(function * () {
  6. console.log(yield genFunc()) // => genFunc
  7. console.log(yield * genFunc()) // => Promise { 'genFunc' }
  8. })

可以看出:genFunc 这个 generatorFunction 在执行后会返回一个 promise,当使用 yield genFunc() 的时候,co 判断返回了一个 promise 会继续帮我们调用它的 then 从而得到真正的字符串。如果使用 yield * genFunc(),就用了语言原生的特性而不经过 co,直接返回一个 promise。

解决方法(任选其一)

  1. function * genFunc -> function genFunc,用 yield genFunc()
  2. return Promise.resolve('genFunc') -> return yield Promise.resolve('genFunc'),用 yield* genFunc()

不过,建议最终转换到 async/await + Promise 上来,毕竟 co + generator 只是一个过渡产物。

3.2.6 async + bluebird

如果是使用 async/await + bluebird 的情况呢?代码如下:

async_bluebird.js

  1. const fs = require('fs')
  2. const profiler = require('v8-profiler')
  3. const Promise = require('bluebird')
  4. async function A () {
  5. return await Promise.resolve('A')
  6. }
  7. async function B () {
  8. return await A()
  9. }
  10. (async function asyncBluebirdWrap () {
  11. const start = Date.now()
  12. profiler.startProfiling()
  13. while (Date.now() - start < 10000) {
  14. await B()
  15. }
  16. const profile = profiler.stopProfiling()
  17. profile.export()
  18. .pipe(fs.createWriteStream('async_bluebird.cpuprofile'))
  19. .on('finish', () => {
  20. profile.delete()
  21. console.error('async_bluebird.cpuprofile export success')
  22. })
  23. })()

结论:调用栈比 co_blueblird.js 的还乱。

3.2.7 参考链接

上一节:3.1 Promise

下一节:3.3 Error Stack