Nodejs是一个事件驱动、异步非阻塞的Javascript平台。异步编程模型是它的主要特色,所以nodejs众多的第三方模块都提供了异步api。
这种方式有很多优点,但在某些情况下,会造成以下问题:
具体如何这里就不多说了,相信每个使用过的人都深有感触,不然怎么会有那么多的模块存在的目的就是为了改善这一点呢?
这些通过三种方式来实现:
经过近两天的查看与试用,我对其中几个比较感兴趣。我的标准是:
下面一一介绍。
https://github.com/maxtaco/tamejs
tamejs不是纯js,因为它增加了两个特有的东西:await/defer。通常把文件保存为.tjs,在运行前需要先编译为.js(也可以通过require某些库在运行期进行)。这里给一个代码示例:
for (var i = 0; i < 10; i++) {
await { setTimeout (defer (), 100); } console.log ("hello");
}
代码看起来相当简洁,为了这个,我宁愿多一个编译步骤!
看看它转换的js代码是什么样的:
var tame = require('tamejs').runtime;
var tame_defer_cb = null;
var tame_fn_0 = function (__tame_k) {tame.setActiveCb (__tame_defer_cb); var __tame_k_implicit = {}; var i = 0; var __tame_fn_1 = function (__tame_k) { tame.setActiveCb (__tame_defer_cb); var __tame_fn_2 = function (__tame_k) { tame.setActiveCb (__tame_defer_cb); i ++ tame.callChain([__tame_fn_1, __tame_k]); tame.setActiveCb (null); }; __tame_k_implicit.k_break = __tame_k; __tame_k_implicit.k_continue = function() { __tame_fn_2(__tame_k); }; if (i < 10) { var __tame_fn_3 = function (__tame_k) { tame.setActiveCb (__tame_defer_cb); var __tame_fn_4 = function (__tame_k) { tame.setActiveCb (__tame_defer_cb); var __tame_defers = new tame.Deferrals (__tame_k); var __tame_fn_5 = function (__tame_k) { tame.setActiveCb (__tame_defer_cb); setTimeout ( __tame_defers.defer ( { parent_cb : __tame_defer_cb, line : 2, file : "d.tjs" } ) , 100 ) ; tame.callChain([__tame_k]); tame.setActiveCb (null); }; __tame_fn_5(tame.end); __tame_defers._fulfill(); tame.setActiveCb (null); }; var __tame_fn_6 = function (__tame_k) { tame.setActiveCb (__tame_defer_cb); console . log ( "hello" ) ; tame.callChain([__tame_k]); tame.setActiveCb (null); }; tame.callChain([__tame_fn_4, __tame_fn_6, __tame_k]); tame.setActiveCb (null); }; tame.callChain([__tame_fn_3, __tame_fn_2, __tame_k]); } else { tame.callChain([__tame_k]); } tame.setActiveCb (null); }; tame.callChain([__tame_fn_1, __tame_k]); tame.setActiveCb (null);
};
__tame_fn_0 (tame.end);
看起来还是有点吓人的,不容易啊不容易。
非常可惜的是,在mocha中无法正常使用。看下面这段代码:
require('should');
function inc(n, callback) {
setTimeout(function() {console.log('### inc: ' + n); callback(n+1);
}, 1000);
};describe('test', function(){
it('show ok with tamejs', function(){console.log('### testing ...'); var result; await { inc(1, defer(result)); } console.log('result: ' + result); result.should.equal(3);
});
});
不知为什么,提示测试通过,实际上inc函数完全没有运行。
提了个bug: https://github.com/maxtaco/tamejs/issues/30,希望能早日解决。
https://github.com/olegp/common-node
Common node基于node-fibers,提供了同步风格的代码,但在内部利用了纤程,不会阻塞主线程。既可以使用同步风格的代码,又不会影响性能,看起来很不错。
这里有一个性能对比图,可见common-node的同步风格,对于性能的影响还是很小的:
common-node的定位应该与node是相同的,但在编程风格上正好相反,它提倡使用“同步”。也因此,对于已有的异步api的模块,它不能很好地直接使用,需要专门为它提供一个修改版。从README中可以看到,目前已经有一些项目支持common-node。可惜相比nodejs庞大的模块库来说,还是可怜了,所以common-node的发展前景还是有些不明朗。
当支持它的模块更多一些之后,也许会有更多人来尝试它的。
(修改:在实际使用中,发现该库与很多其它的库有冲突,出现各种各样的错误,故不再推荐。现在使用async替代,见下段)
https://github.com/goodeggs/fibrous
Fibrous将给每个对象增加一个sync和feature属性,给原有的各异步方法提供一个同步版。因为它也基于node-fibers,所以这个同步函数实际上将在一个纤程中执行,不会阻塞主线程。
它的代码写起来比较简洁,看示例:
var fibrous = require('fibrous');
var crypto = require('crypto');var random = fibrous(function(length) {
var buf = crypto.sync.randomBytes(length); return buf.toString('hex');
});
random(4,function(err, result) {
console.log(result);
});
如果我们的代码被fibrous()包着,则可在内部调用其它对象的.sync中的某方法(该方法是原异步方法的同步版本),不需要传入回调函数,直接以返回值形式拿到。
如果我们的代码没有被fibrous包着,则还得老老实实传回调。但这不是大问题,因为大多数情况下,我们都可以用fibrous包起来。
再看看它与mocha结合使用:
require('should');
var fibrous = require('fibrous');function inc(n, callback) {
setTimeout(function() { console.log('### inc: ' + n); callback(null, n+1); }, 1000);
};
describe('test', function(){
it('show ok with tamejs', fibrous(function(){ var x = inc.sync.call(null, 5); console.log('x:'+x); }));
});```
注意it('..', fibrous(..)),可以直接把测试部分给包起来,内部就可以使用inc的sync版了。执行这个测试,将正确打印出:
x:6
使用fibrous,虽然对js中的对象有些侵入(增加了sync和futrue属性),但是很多时候,可以让我们的代码更加简洁,这个代价是我完全可以承受的。我将在以后更多的情况下使用它,看看会不会有其它问题。
暂时不用再纠结于异步同步的问题了,因为所有能试的方法我基本上都试了一遍,如果连fibrous也不行,那我也就只能放弃,强迫自己去写回调了。
https://github.com/caolan/async
前面说中了,fibrous在使用中,与很多库有冲突,无奈移除,只好再硬着头皮写嵌套。期间纠结得想换语言,直到用了async。
async设计得巧妙又周到,考虑到了很多场景,能够很大程度上消除嵌套。比如说三种常见的情况:
另外还有7种:)
还有一种常见场景,需要对一个数组中的每一个元素进行异步操作,这时又提供了十种操作:
详细的使用方法,我正在边学习边写示例,发到github上:https://github.com/freewind/async_demo