node.js最主要的特色就是結合非同步I/O以及Event Loop來達到在高負載仍能有很好的反應速度,但是非同步操作常常會中斷流程,而且不保證執行的順序...
非同步操作
非同步操作的問題,是常見的node.js學習障礙。雖然在I/O時使用非同步的操作方式可以提升效能,但是他會中斷目前的邏輯,又不保證執行結果的順序,對於初學者來說,往往會成為困擾。
例如在之前的model設計裡,需要組合出view所需要的所有資訊,但是這樣需要執行好幾次查詢,而每次查詢都是非同步的,等到查詢完畢,其實呼叫查詢的函數早就結束了。那這樣要怎麼把資料組合好再傳給view呢?
Javascript最強大的特色之一,就是他的函數式語言的特性,可以利用這些特性來解決一些特殊的問題。他的函數可以接受另一個函數(或自己)作為參數,也可以返回函數。利用這個特性,我們可以用一些邏輯把函數組合起來,克服非同步的問題。
例子
在實際用mysql做查詢的model中,需要使用兩次query來得到結果,然後丟給view處理。例如:
this.execute = function() {
//第一次查詢
client.query("SELECT `value` as title FROM `configs` WHERE name='title'", function(err, results, fields) {
if(!err) {
sync.next(results);//定義在Wait中的函數,下詳
} else {
console.log('err 1.');
sync.next({});
}
});
//第二次查詢
client.query("SELECT a.name AS `group`, b.name AS name, b.email AS email FROM groups AS a LEFT JOIN authors AS b ON b.group_id=a.id", function(err, results, fields) {
if(!err) {
sync.next(results);
} else {
console.log('err 2.');
sync.next({});
}
});
};
每次呼叫client.query,都需要傳給他一個callback函數,來處理查詢的結果,但是這兩個callback函數是獨立的,也不知道誰會先執行完畢...為了能等到所有的查詢都執行完畢,再把收集起來的結果用view來處理,這時候就需要用別的方法:
function Wait(count, cb) {
var payload = [];
this.next = function(data) {
payload.push(data);//將執行結果存放在payload變數
count--;//每執行一次就遞減
if(count<=0) {
cb(payload);//歸零時就執行callback函數
}
}
}
這個Wait函數其實很簡單,把它當constructor時,可以接收兩個參數,count表示要執行的次數,cb是用來處理結果的函數。next成員函數需要在每個要同步(或者說是等待)的函數最後呼叫,並且把結果傳給他存放。呼叫next時,計數器會減一,當所有動作執行完畢(計數器歸零),就執行傳入的callback函數來處理所有存放的結果。
所以在下面的程式中把要執行的次數以及最後處理結果的函數傳給他:
var sync = new Wait(2, function(obj) {
var i=0;
var ret = {};
for(; i<obj.length; i++) {
if(obj[i][0].title) {//結果的順序無法保證,所以需要做一些偵測再處理查詢結果
ret.title = obj[i].title;
} else {
ret.rows = obj[i]
}
}
client.end();//關閉資料庫連線
view.render(ret);//呼叫view來畫出結果
});
邏輯不複雜,但是就可以讓我的兩次query結束後,才把結果交給view來顯示。
另一個用到這個方式來解決問題的地方,在處理伺服器流程的程式。pre dispatch、dipatch、post dispatch是必須依序執行的流程,但是pre dispatch中是不定數目的函數,可能會用同步或非同步的方式處理header資訊,所以需要「等」到他們執行完畢,才進入dispatch流程。因為無法預先知道pre dispatch有多少函數要執行(這是伺服器開始執行時動態加入的),所以需要調整一下:
var wait = function(callbacks, req, res, done) {
var counter = callbacks.length;//所有的pre dispatch函數都放在一個陣列裡傳進來
var results = [];
var next = function(result) {
results.push(result);
if(--counter < 1) {
done(results);//都執行完畢後,就會呼叫done來繼續流程
}
};
if(counter === 0) {//甚麼都沒有,直接進入下一個流程
done(results);
return;
}
for(var i = 0; i < callbacks.length; i++) {
callbacks[i](req, res, next);//依序執行pre dispatch函數
}
};
這樣在主程式裡面呼叫他:
wait(pre, request, response, function(){
if(dispatched) {
return;
} else {
dispatched = true;
//觸發dispatch事件,進入dispatch流程
server.emit('dispatch', conf, request, response, tools.serverRender(that, request, response));
}
});
就能達成順序執行這兩個流程的目的。
實用的模組
更複雜的流程,最好利用一些別人做好的更成熟的模組。在node.js的wiki中,有一頁模組列表,其中一個分類就是流程控制:
https://github.com/joyent/node/wiki/Modules#wiki-async-flow
裡面可以發現非常多的協助你做非同步流程控制的模組,如果不知道要用哪一個,建議可以先嘗試:
https://github.com/caolan/async
這是使用Javascript來解決Javascript的問題。其實還有另一種approach,是使用不同的語法,例如:
https://github.com/Sage/streamlinejs
它讓你用「同步」的Javascript語法來執行「非同步」的程式,他需要經過一個編譯過程,把傳給函數的"_"參數擴展開來變成非同步的形式。
非同步與循序邏輯的衝突,是開發node.js一定會碰到的問題,這時候還是要依靠這些好用的模組來幫忙。