What’s wrong with Promise?
Promsie 刚出来的时候欣喜若狂,终于可以摆脱万恶的回调嵌套了。但最近项目中越来越觉得Promise不合场景,当业务逻辑复杂,分支情况变多时代码结构越来越难看。Promise 点then .then的方式注定会依次往下执行, 一同事为了直接返回,使用Promise.reject()强行跳转,感觉又看到了goto的影子.(go die …).
然后决定试用一波co模块(tj 的co ,bluebird 的coroutine,还有 when/generator这三个模块有一个共同的特点,yield之后的语句需要返回一个Promise. Promise 是为了解决回调操作带来的麻烦的,coroutine从某种意义上讲也是为了解决回调带来的代码可读性问题,那为何还要依赖于Promise呢,TJ在co
这个项目里的回答是Promise变得越来越流行,并且官方化了,所以co
也加入了这个潮流。私以为还是得从原本的目的出发,几行代码能解决的问题,没必要套个Promise.
基于以上考虑开发了zco 这个模块,除了能和callback无缝使用和必须的feature,还引入了golang里面的defer功能,从获取函数返回值得形式上看也很像golang(这个无心插柳。。)。下面贴出了性能测试,也附上了做同样的事各个coroutine版本的差异。
benchmark
几种书写异步代码方式的性能测试结果,其中co 前缀的是指coroutine版本。 对于coroutine的版本 ,比较了zco ,ES7 async/await ,tj’s co, bluebird coroutine, when/generator 五种,其中zco是表现最好的。我也有点意外,为什么es7 的async/await 性能一般。测试代码见 https://github.com/yyrdl/zco .
name timecost(ms) memery(mb)
callback.js 116 30.30078125
[email protected] 187 48.52734375
[email protected] 713 93.58984375
[email protected] 800 76.375
[email protected] 1100 122.515625
[email protected] 1159 118.69921875
[email protected] 1359 136.29296875
[email protected] 1386 125.58203125
promise_native.js 1451 171.72265625
async_await_es7_with_native_promise.js 1526 170.234375
[email protected] 1720 165.0703125
[email protected] 1753 162.3203125
async_await_es7_with_bluebird_promise.js 1891 197.7109375
[email protected] 4446 244.984375
Platform info:
Windows_NT 10.0.14393 x64
Node.JS 7.7.3
V8 5.5.372.41
Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz × 4
zco
灵感来自于tj的co,创新之处在于不需要Promise了,该模块允许你书写同步代码直接调用异步方法。以返回某一个文件夹下的js 文件列表为例,依次比较zco 版,tj co版,Promise版,纯回调版,es7 async wait五种写法:
zco 版
const zco=require("zco");
const fs=require("fs");
const testDirectory="./cases";
let getAllJsFileZCOVersion=function(dirname){
return zco(function*(next){
let files=[];
let [err,list]=yield fs.readdir(dirname,next);//获得目录下的文件列表
if(err){
throw err;
}
for(var i=0;i<list.length;i++){
let [er,stat]=yield fs.stat(dirname+"/"+list[i],next);//获得文件的信息
if(er){
throw er;
}
if(stat.isFile()&&list[i].endsWith(".js")){//判断是不是js文件
files.push(list[i]);
}
}
return files;
});
}
//then use it
zco(function*(next){
var [err,jsFiles]=yield getAllJsFileZCOVersion(testDirectory)(next);
if(err){
console.log(err.message);
}else{
console.log(jsFiles);
}
})();
//或者这样用
getAllJsFileZCOVersion(testDirectory)((err,jsFiles)=>{
if(err){
console.log(err.message);
}else{
console.log(jsFiles);
}
})
tj co 版
const co=require("co");
const Promise=require("bluebird");
const fs=require("fs");
const testDirectory="./cases";
//需要将原本的回调操作包装成promise的版本
let readdir=function(dirname){
return new Promise((resolve,reject)=>{
fs.readdir(dirname,(err,list)=>{
if(err){
reject(err);
}else{
resolve(list);
}
})
})
}
let stat=function(file){
return new Promise((resolve,reject)=>{
fs.stat(file,(err,stats)=>{
if(err){
reject(err);
}else{
resolve(stats);
}
})
});
}
let getAllJsFileTJCOVersion=function(dirname){
return co(function*(){
let list=yield readdir(dirname);
let files=[];
for(let i=0;i<list.length;i++){
let stats=yield stat(dirname+"/"+list[i]);
if(stats.isFile()&&list[i].endsWith(".js")){
files.push(list[i]);
}
}
return files;
});
}
//then use it
getAllJsFileTJCOVersion(testDirectory).then((files)=>{
console.log(files);
}).catch((err)=>{
console.log(err);
})
Promise 版
const Promise=require("bluebird");
const fs=require("fs");
const testDirectory="./cases";
let readdir=function(dirname){
return new Promise((resolve,reject)=>{
fs.readdir(dirname,(err,list)=>{
if(err){
reject(err);
}else{
resolve(list);
}
})
})
}
let stat=function(file){
return new Promise((resolve,reject)=>{
fs.stat(file,(err,stats)=>{
if(err){
reject(err);
}else{
resolve(stats);
}
})
});
}
let getAllJsFilePurePromiseVersion=function (dirname) {
return readdir(dirname).then((list)=>{
let pros=[];
for(let i=0;i<list.length;i++){
pros.push(stat(dirname+"/"+list[i]));
}
return Promise.all(pros).then((statsList)=>{
let files=[];
for(let i=0;i<statsList.length;i++){
if(statsList[i].isFile()&&list[i].endsWith(".js")){
files.push(list[i]);
}
}
return files;
});
});
}
//then use it
getAllJsFilePurePromiseVersion(testDirectory).then((files)=>{
console.log(files);
}).catch((err)=>{
console.log(err);
})
纯回调版
回调是Node.js 里面最原始的用法,也是性能最好的用法
let getAllJsFilePureCallbackVersion=function(dirname,callback){
let index=0,list=[],files=[];
let alreadyReturn=false;
let _end=function (err) {
if(!alreadyReturn){
alreadyReturn=true;
err?callback(err):callback(undefined,files);
}
}
let checkDone=function () {
if(index===list.length){
_end();
}
}
let jsFile=function () {
for(let i=0;i<list.length;i++){
((j)=>{
fs.stat(dirname+"/"+list[j],(err,stats)=>{
if(err){
_end(err)
}else if(stats.isFile()&&list[j].endsWith(".js")){
files.push(list[j]);
index++;
checkDone();
}
})
})(i)
}
}
fs.readdir(dirname,(err,_list)=>{
if(err){
_end(err);
}else{
list=_list;
jsFile();
}
});
}
//then use it
getAllJsFilePureCallbackVersion(testDirectory,(err,files)=>{
if(err){
console.log(err);
}else{
console.log(files);
}
})
es7 async await 版本
const fs=require("fs");
const testDirectory="./cases";
//async await 需要和Promise一起用,需要将原本的回调包装成Promise
let readdir=function(dirname){
return new Promise((resolve,reject)=>{
fs.readdir(dirname,(err,list)=>{
if(err){
reject(err);
}else{
resolve(list);
}
})
})
}
let stat=function(file){
return new Promise((resolve,reject)=>{
fs.stat(file,(err,stats)=>{
if(err){
reject(err);
}else{
resolve(stats);
}
})
});
}
let getAllJsFileAsyncAwaitES7Version=async function (dirname) {
let list=await readdir(dirname);
let files=[];
for(let i=0;i<list.length;i++){
let stats=await stat(dirname+"/"+list[i]);
if(stats.isFile()&&list[i].endsWith(".js")){
files.push(list[i]);
}
}
return files;
}
getAllJsFileAsyncAwaitES7Version(testDirectory).then((files)=>{
console.log(files);
}).catch((err)=>{
console.log(err);
})
这四种方式,如果考虑性能要求的话个人会选择纯回调的的写法,如果性能要求不那么高,更喜欢 zco的版本,因为更简洁。
根据性能测试结果, zco 是这几个coroutine模块中性能最好的,若追求极限性能的话还是精心设计回调吧,项目地址:https://github.com/yyrdl/zco
目前是稳定版本,亲们都知道的,求star ,客官认为还可以的话就给个呗,当然更欢迎批评意见! ^-^
然并卵,用上async+await你会爽翻的
@zengming00 明天更新node到最新版本试用一波传说中的 async await
nodejs的姿势和花样越来越多了
这样写感觉更恶心
来自酷炫的 CNodeMD
@WilliamDu1981 根据兄台的评论,重写了用例,比较了zco ,tj co ,Promise ,纯回调四种写法,并认为 zco版本是最简洁的,回调版是运行性能最好的,已经加上四种版本的代码
目前最好的方案是async/await.
@shoyer2010 已经做了比较,并附上了benchmark 和 async/await 的case
@zengming00 已经做了比较,并附上了benchmark 和 async/await 的case
tj co 的版本,要配合 https://www.npmjs.com/package/mz 才公平
@atian25 嗯嗯,书写时代码量会少一点。但性能不会提高。mz是在fs上又包了一层Promise
首先, 你这种探索的精神值得肯定, 能花这么多时间和精力去研究比较, 是一种好的学习方法, 但我还是要指出你的方案的问题, 首先,你的方法无法摆脱callback参数 , 即在异步操作的时候,你的函数里,必须提供一个callback参数, 也就是你在每个异步函数的时候要传入一个next参数, 这种写法是丑陋的写法, 可读性差, 不直观, 不利于维护,写点小项目还行, 写大点的工程, 这个缺点就会被放大, async/await的出现就是为了解决这个问题. 另外你的异步方法内必须调用callback, 如果忘记调用就会卡住, 在实际代码中, 可能我会把每个函数都定义为异步,但里面完全是同步代码, async/await 没有这个问题.这种情况下,你的方案会出问题. 很多时候性能并不是我们追求的唯一目标, 我们写代码的时候必须考虑代码的可读性, 可维护性, 引用一句别人说的, 代码是用来读的, 顺便执行一下, 所以我们要在可读性和性能之间作出平衡. 如果一定要在这两者之间作一个选择, 那么应该是在保证代码可读性的前提下,去提高性能, 而且对服务器程序来说, 一般性能瓶颈都不是在代码层面, 是在数据库上. 最后, 还需要提醒你一点, 你这样的基准测试是不公平的, 因为你的这个代码功能太少, 包括一些异常情况也没处理, 而其他方案基本都是完整的整套异步解决方案, 代码量要多, 功能也很多, 加载到内存中, 消耗的资源自然会不一样. 如果是你的库也实现了其他库一样的完整功能的话, 这样的对比测试才具有意义. 下面是两种代码的对比,你可以参考下,继续你的探索之路.
const co=require("zco");
let sleep01 = function (milliseconds, callback) {
setTimeout(function() {
callback();
}, milliseconds);
}
let async_func1=function(callback){
let error=undefined;
setTimeout(()=>{
callback(error,"hello world");
},1000)
}
let async_func2=function(callback){
let error=undefined;
callback(null, '32323');
// setTimeout(()=>{
// callback(error,"hello world2");
// },1000)
}
/*****************************simple use***************************/
co(function *(next) {
let [err,str]=yield async_func1(next);
console.log(str);//"hello world"
yield sleep01(2000, next);
[err,str]=yield async_func2(next);
console.log(str);
})();
//----------------------------------------------------------------------
// async / await
const sleep = async milliseconds => new Promise((resolve, reject) => setTimeout(resolve, milliseconds));
const async_f1 = async function () {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve('from f1');
}, 1000);
});
};
const async_f2 = async function () { // 注意: 这里面并没有异步操作, 但我可以在任何时候,加入异步代码,而不影响其他部份的代码.
return 2323;
};
const async_f3 = async function () {
return new Promise((resolve, reject) => {
resolve('from f3');
});
};
(async function () {
let result1 = await async_f1();
console.log(result1);
await sleep(3000);
let result2 = await async_f2();
console.log(result2);
let result3 = await async_f3();
console.log(result3);
})();
@shoyer2010 谢谢您肯花时间在这个话题上。:)
该模块确实不能摆脱callback,而事实上node里面所有的异步操作都是callback,大家常用的Promise从实现来讲就是callback。对于在异步方法里没有调用 callback就像是Promise里面没有resolve一样。zco唯一的缺憾是每次yield 都需要传next。然后对于异步操作,函数里并不一定要提供一个callback参数.举个栗子:
const fs=require("fs");
const co=require("zco");
let readFile=function(path){//这是一个异步操作,但没有显示的callback
return co(function*(next){//类似于用promise包装,其实没必要包装,直接调用原本的fs api就行
let [err,data]=yield fs.readFile(path,next);
if(err){
throw err;//或者在这里做一些其他操作;
}
return data;
})
}
co(function*(next){
let [err,data]=yield readFile("./test.js")(next);
//..........
})()
然后是异常捕获的问题,直接用你的例子吧:
co(function *(next) {
let [err,str]=yield async_func1(next);
console.log(str);//"hello world"
yield sleep01(2000, next);
[err,str]=yield async_func2(next);
console.log(str);
})((err,returnValue)=>{
if(err){
console.log(err);//"Generator is already running"
}else{
console.log(returnValue);
}
});
这里如果没有传最后的handler 异常应该被抛出而不应该被吞掉,这个是我的过失,之后修改。(Promise最后没有加catch ,异常也是被吞掉了) 然后对于为什么会异常,readme.md上 Important那儿有写。然后同步代码不必await的 ,如果需要,那岂不是每一次函数调用都得加个await了。
您举的zco版的 async_fun2 的定义应该和 async/await 版的定义一样,调用的时候前面不用加 yield ,直接就是 let data=async_func2()
,因为那并不是一个异步操作。
然后写法丑陋的问题,这个不作辩解,因为是个见仁见智的问题, 就像 “{” 的位置该放哪儿一样。但私以为至少比promise版本优雅很多
@yyrdl let data=async_func2() 这种我只是举例说明, 这个函数里虽然没有异步操作, 但某些情况下我也会认为这是一个异步函数, 所以要加上await调用, 也就是加上await可以兼容同步和异步两种代码, 这在某些场景下是非常有用的, 这样的好处是, 以后这个函数里出现await的时候, 我不再需要去调用async_func2()的地方再加上await .如果 不加, 那么如果 以后async_func2里面出现了异步代码, 你就得去其他调用这个函数的前面加await关键字.
嗯嗯,你说的是对的。对于一个func1 ,假如里面的有分支,其中一个分支是同步调用callback,而另一个分支则是异步,这样的场景很常见,需要支持。 谢谢你的issue,已经 更新到1.2.1版本,支持伪异步。原理是运行时检查是不是一个异步操作。performance 依然是corouine模块中最好的一个。
有兴趣的话可以一起维护啊 :)
@yyrdl 我平时工作很忙,偶尔上来看看, 只能干吐槽这样的活儿^_^
@shoyer2010 我也在工作,老板会不会打我 T_T
@shoyer2010 哈罗,您在评论中提了很多建设性的意见,zco 已经更新到1.3.2版本,克服了原有的不足,并增加了新的功能,其中突出的是提供完整的错误堆栈(node里的异步调用会导致堆不完整栈),兄台再给几句金玉良言?^_^