NodeJS -- 异步编程
NodeJS最大的卖点--事件机制和异步IO,对开发者并不透明
代码设计模式
var output = fn1(fn2('input')); // Do something;
在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递,因此一般按以下方式编写代码:
fn2('input', function(output2) { fn1(output2, function(output1) { //Do something. }); });
var len = arr.length, i = 0; for(;i < len; ++i) { arr[i] = sync(arr[i]); } //All array items have processed.
若是异步执行,以上代码就无法保证循环结束后所有数组成员都处理完毕了,如果数组成员必须一个接一个串行处理,则按以下方式编写代码:
(function next(i, len, callback) { if(i < len) { async(arr[i], function(value) { arr[i] = value; next(i + 1, len, callback); }); } else { callback(); } }(0, arr.length, function() { // All array items have processed. }));
可以看到,在异步函数执行一次并返回执行结果后才传入下一个数组成员并开始下一轮执行,直到所有数组成员处理完毕后,通过回调的方式触发后续代码执行。
若数组成员可以并行处理,但后续代码仍然需要所有数组成员处理完毕后才能执行的话,则异步代码调整成以下形式:
(function(i, len, count, callback) { for(;i < len; ++i) { (function(i) { async(arr[i], function(value) { arr[i] = value; if(++count === len) { callback(); } }); }(i)); } }(0, arr.length, 0, function() { //All array items have processed. }));
以上代码并行处理所有成员,并通过计数器变量来判断什么时候所有数组成员都处理完毕了
function async(fn, callback) { // Code execution path breaks here. setTimeout(function() { callback(fn()); }, 0); } try { async(null, function(data) { // Do something. }); } catch(err) { console.log('Error: %s', err.message); } -----------------------Console---------------------------- E:LanguageJavascriptNodeJS ry_catch.js:25 callback(fn()); ^ TypeError: fn is not a function at null._onTimeout (E:LanguageJavascriptNodeJS ry_catch.js:25:18) at Timer.listOnTimeout (timers.js:92:15)
因为代码执行路径被打断了,我们就需要在异常冒泡到断点之前用try语句把异常捕获注,并通过回调函数传递被捕获的异常,改造:
function async(fn, callback) { // Code execution path breaks here. setTimeout(function() { try { callback(null, fn()); } catch(err) { callback(err); } }, 0); } async(null, function(err, data) { if(err) { console.log('Error: %s', err.message); } else { // Do something } }) ----------------------Console---------------------- Error: fn is not a function
function main(callback) { // Do something. asyncA(function(err, data) { if(err) { callback(err); } else { // Do something. asyncB(function(err, data) { if(err) { callback(err); } else { // Do something. asyncC(function(err, data) { if(err) { callback(err); } else { // Do something. callback(null); } }); } }); } }); } main(function(err) { if(err) { // Deal with exception. } });
回调函数已经让代码变得复杂了,而异步之下的异常处理更加剧了代码的复杂度,幸好,NodeJS提供了一些解决方案。
域
process.on('uncaughtException', function(err) { console.log('Error: %s', err.message); }); setTimeout(function(fn) { fn(); }); ------------------------Console-------------------- Error: fn is not a function
function async(req, callback) { // Do something. asyncA(req, function(err, data) { if(err) { callback(err); } else { // Do something. asyncB(req, function(err, data) { if(err) { callback(err); } else { // Do something. asyncC(req, function(err, data) { if(err) { callback(err); } else { // Do something. callback(null, data); } }); } }); } }); } http.createServer(function(req, res) { async(req, function(err, data) { if(err) { res.writeHead(500); res.end(); } else { res.writeHead(200); res.end(); } }); });
以上将请求对象交给异步函数处理,再根据处理结果返回响应,这里采用了使用回调函数传递异常的方案,因此async函数内部若再多几个异步函数调用的话,代码就更难看了,为了让代码好看点,可以在没处理一个请求时,使用domain模块创建一个子域,在子域内运行的代码可以随意抛出异常,而这些异常可以通过子域对象的error事件统一捕获,改造:
function async(req, callback) { // Do something. asyncA(req, function(data) { // Do something. asyncB(req, function(data) { // Do something. asyncC(req, function(data) { // Do something. callback(data); }); }); }); } http.createServer(function(req, res) { var d = domain.create(); d.on('error', function() { res.writeHead(500); res.end(); }); d.run(function() { async(req, function(data) { res.writeHead(200); res.end(data); }); }); });
注:JS的throw...tyr...catch异常处理机制并不会导致内存泄漏和使程序执行出乎意料,而是因为NodeJS并不是纯粹的JS,NodeJS里大量的API内部是用C/C++实现的,因此NodeJS程序运行过程中,代码执行路径穿梭于JS引擎内外部,而JS异常抛出机制可能打断正常代码的执行流程,导致C/C++部分的代码表现异常,进而导致内存泄漏。
因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,若不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当,而使用try语句捕获的异常一般是JS本身的异常,不用担心上述问题