Say Bye Bye to Promise . And Compare With Async Await (ES7 feature)
发布于 2 年前 作者 yyrdl 3047 次浏览 来自 分享

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 ,客官认为还可以的话就给个呗,当然更欢迎批评意见! ^-^

17 回复

然并卵,用上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关键字.

@shoyer2010

嗯嗯,你说的是对的。对于一个func1 ,假如里面的有分支,其中一个分支是同步调用callback,而另一个分支则是异步,这样的场景很常见,需要支持。 谢谢你的issue,已经 更新到1.2.1版本,支持伪异步。原理是运行时检查是不是一个异步操作。performance 依然是corouine模块中最好的一个。

有兴趣的话可以一起维护啊 :)

@yyrdl 我平时工作很忙,偶尔上来看看, 只能干吐槽这样的活儿^_^

@shoyer2010 我也在工作,老板会不会打我 T_T

@shoyer2010 哈罗,您在评论中提了很多建设性的意见,zco 已经更新到1.3.2版本,克服了原有的不足,并增加了新的功能,其中突出的是提供完整的错误堆栈(node里的异步调用会导致堆不完整栈),兄台再给几句金玉良言?^_^

回到顶部