笔者很长一段时间都使用 koa@1 +(generator|bluebird)+ sequelize 这个组合,这个组合并没有什么问题,也很常见,但是到了滥用的地步,导致后来维护和调试都很痛苦。排除掉 sequelize 这个不得不用的模块,从调试 cpuprofile 角度讲讲为什么我认为应该用 async/await + Promise 替代 co + generator|bluebird。
我的观点是:使用原生模块具有更清晰的调用栈。
下面用 4 个例子对比,看下相同逻辑不同代码生成的 cpuprofile 中调用栈的信息。
async.js
const fs = require('fs')
const profiler = require('v8-profiler')
async function A () {
return await Promise.resolve('A')
}
async function B () {
return await A()
}
(async function asyncWrap () {
const start = Date.now()
profiler.startProfiling()
while (Date.now() - start < 10000) {
await B()
}
const profile = profiler.stopProfiling()
profile.export()
.pipe(fs.createWriteStream('async.cpuprofile'))
.on('finish', () => {
profile.delete()
console.error('async.cpuprofile export success')
})
})()
加载运行后生成的 async.cpuprofile,如下所示:
可以看出:asyncWrap 中调用了 B 函数,B 函数调用了 A 函数,A 函数中 resolve 了一个值。asyncWrap 中还调用了 stopProfiling 函数。
co.js
const fs = require('fs')
const co = require('co')
const profiler = require('v8-profiler')
function * A () {
return yield Promise.resolve('A')
}
function * B () {
return yield A()
}
co(function * coWrap () {
const start = Date.now()
profiler.startProfiling()
while (Date.now() - start < 10000) {
yield B()
}
const profile = profiler.stopProfiling()
profile.export()
.pipe(fs.createWriteStream('co.cpuprofile'))
.on('finish', () => {
profile.delete()
console.error('co.cpuprofile export success')
})
})
加载运行后生成的 co.cpuprofile,如下所示:
可以看出:调用栈非常深,太多没有用的 co 相关的调用栈。如果 n 个 generator 层层嵌套,就会出现 n 倍的 (anonymous)->onFullfiled->next->toPromise->co->Promise->(anonymous) 调用栈。如果你读过 co 的源码可能知道,这是 co 将 generator 解包的过程。其实这个可以将 yield generator
替换成 yield* generator
来优化。
co_better.js
const fs = require('fs')
const co = require('co')
const profiler = require('v8-profiler')
function * A () {
return yield Promise.resolve('A')
}
function * B () {
return yield * A()
}
co(function * coWrap () {
const start = Date.now()
profiler.startProfiling()
while (Date.now() - start < 10000) {
yield * B()
}
const profile = profiler.stopProfiling()
profile.export()
.pipe(fs.createWriteStream('co_better.cpuprofile'))
.on('finish', () => {
profile.delete()
console.error('co_better.cpuprofile export success')
})
})
加载运行后生成的 co_better.cpuprofile,如下所示:
可以看出:相比 co.js 调用栈就清晰了很多,不过相比用 async/await 还是多了些 onFulfilled、next。
co_bluebird.js
const fs = require('fs')
const co = require('co')
const Promise = require('bluebird')
const profiler = require('v8-profiler')
function * A () {
return yield Promise.resolve('A')
}
function * B () {
return yield * A()
}
co(function * coBluebirdWrap () {
const start = Date.now()
profiler.startProfiling()
while (Date.now() - start < 10000) {
yield * B()
}
const profile = profiler.stopProfiling()
profile.export()
.pipe(fs.createWriteStream('co_bluebird.cpuprofile'))
.on('finish', () => {
profile.delete()
console.error('co_bluebird.cpuprofile export success')
})
})
加载运行后生成的 co_bluebird.cpuprofile,如下所示:
可以看出:相比较 co_better.js,调用栈中多了许多 bluebird 模块的无用信息。而且这只是非常简单的示例代码,要是复杂的业务逻辑大量使用 bluebird 代码生成的 cpuprofile,几乎没法看了。
结论:使用 async/await + Promise + 命名函数,具有更清晰的调用栈,让分析 cpuprofile 时不再痛苦。
聪明的你可能会问:
- 为什么不建议用 bluebird?原因是:
- 随着 V8 不断优化,原生 Promise 性能逐渐提高,bluebird 性能优势不明显
- 原生 Promise 的 API 足够用,至少能 cover 大部分使用场景,而且还在不断完善,未来还会添加新的 api,如:Promise.prototype.finally。
- 具有更清晰的调用栈
- 由于历史遗留原因,现在代码中大量使用了 yield + generator 怎么办?
- 将所有 yield generator 替换成 yield * generator
- 升级到 node@8+,逐步用 async/await 替换,毕竟 async 函数调用后返回的也是一个 promise 嘛,也是 yieldable 的
- 性能比较呢?node@8+ 下 async/await 完胜 co,见参考链接
上面讲到可以将 yield generator -> yield * generator,这里面有一个坑,是由于不明白 co 的原理滥用 co 导致的。代码如下:
const co = require('co')
function * genFunc () {
return Promise.resolve('genFunc')
}
co(function * () {
console.log(yield genFunc()) // => genFunc
console.log(yield * genFunc()) // => Promise { 'genFunc' }
})
可以看出:一个 generatorFunction 执行后返回一个 promise,当使用 yield 的时候,co 判断返回了一个 promise 会继续帮我们调用它的 then 得到真正的字符串。如果使用 yield *,即用了语言原生的特性而不经过 co,直接返回一个 promise。
解决方法(任选其一):
function * genFunc
->function genFunc
,用yield genFunc()
return Promise.resolve('genFunc')
->return yield Promise.resolve('genFunc')
,用yield* genFunc()
不过,建议最终转换到 async/await + Promise 上来,毕竟 co + generator 只是一个过渡产物。
如果是使用 async/await + bluebird 的情况呢?
async_bluebird.js
const fs = require('fs')
const profiler = require('v8-profiler')
const Promise = require('bluebird')
async function A () {
return await Promise.resolve('A')
}
async function B () {
return await A()
}
(async function asyncBluebirdWrap () {
const start = Date.now()
profiler.startProfiling()
while (Date.now() - start < 10000) {
await B()
}
const profile = profiler.stopProfiling()
profile.export()
.pipe(fs.createWriteStream('async_bluebird.cpuprofile'))
.on('finish', () => {
profile.delete()
console.error('async_bluebird.cpuprofile export success')
})
})()
结论:调用栈比 co_blueblird.js 的还乱。
上一节:3.1 Promise
下一节:3.3 Error Stack