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:\Language\Javascript\NodeJS\try_catch.js:25 callback(fn()); ^ TypeError: fn is not a function at null._onTimeout (E:\Language\Javascript\NodeJS\try_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本身的異常,不用擔心上述問題