非同步程式設計帶來的問題在客戶端Javascript中並不明顯,但隨著伺服器端Javascript越來越廣的被使用,大量的非同步IO操作使得該問題變得明顯。許多不同的方法都可以解決這個問題,本文討論了一些方法,但並不深入。大家需要根據自己的情況選擇一個適於自己的方法。
筆者在之前的一片部落格中簡單的討論了Python和Javascript的異同,其實作為一種程式語言Javascript的非同步程式設計是一個非常值得討論的有趣話題。
JavaScript 非同步程式設計簡介
回呼函式和非同步執行
所謂的非同步指的是函式的呼叫並不直接傳回執行的結果,而往往是透過回呼函式非同步的執行。
我們先看看回呼函式是什麼:
var fn = function(callback) {
// do something here
…
callback.apply(this, para);
};
var mycallback = function(parameter) {
// do someting in customer callback
};
// call the fn with callback as parameter
fn(mycallback);
回呼函式,其實就是呼叫使用者提供的函式,該函式往往是以引數的形式提供的。回呼函式並不一定是非同步執行的。比如上述的例子中,回呼函式是被同步執行的。大部分語言都支援回呼,C++可用透過函式指標或者回呼物件,Java一般也是使用回呼物件。
在Javascript中有很多透過回呼函式來執行的非同步呼叫,例如setTimeout()或者setInterval()。
setTimeout(function(){
console.log(“this will be exectued after 1 second!”);
},1000);
在以上的例子中,setTimeout直接傳回,匿名函式會在1000毫秒(不一定能保證是1000毫秒)後非同步觸發並執行,完成列印控制檯的操作。也就是說在非同步操作的情境下,函式直接傳回,把控制權交給回呼函式,回呼函式會在以後的某一個時間片被排程執行。那麼為什麼需要非同步呢?為什麼不能直接在當前函式中完成操作呢?這就需要瞭解Javascript的執行緒模型了。
Javascript執行緒模型和事件驅動
Javascript最初是被設計成在瀏覽器中輔助提供HTML的互動功能。在瀏覽器中都包含一個Javascript引擎,Javscript程式就執行在這個引擎之中,並且只有一個執行緒。單執行緒能都帶來很多優點,程式員們可以很開心的不用去考慮諸如資源同步,死鎖等多執行緒阻塞式程式設計所需要面對的惱人的問題。但是很多人會問,既然Javascript是單執行緒的,那它又如何能夠非同步的執行呢?
這就需要瞭解到Javascript在瀏覽器中的事件驅動(event driven)機制。事件驅動一般透過事件迴圈(event loop)和事件佇列(event queue)來實現的。假定瀏覽器中有一個專門用於事件排程的實體(該實體可以是一個執行緒,我們可以稱之為事件分發執行緒event dispatch thread),該實體的工作就是一個不結束的迴圈,從事件佇列中取出事件,處理所有很事件關聯的回呼函式(event handler)。註意回呼函式是在Javascript的主執行緒中執行的,而非事件分發執行緒中,以保證事件處理不會發生阻塞。
Event Loop Code:
while(true) {
var event = eventQueue.pop();
if(event && event.handler) {
event.handler.execute(); // execute the callback in Javascript thread
} else {
sleep(); //sleep some time to release the CPU do other stuff
}
}
透過事件驅動機制,我們可以想象Javascript的程式設計模型就是響應一系列的事件,執行對應的回呼函式。很多UI框架都採用這樣的模型(例如Java Swing)。
那為什要非同步呢,同步不是很好麼?
非同步的主要目的是處理非阻塞,在和HTML互動的過程中,會需要一些IO操作(典型的就是Ajax請求,指令碼檔案載入),如果這些操作是同步的,就會阻塞其它操作,使用者的體驗就是頁面失去了響應。
綜上所述Javascript透過事件驅動機制,在單執行緒模型下,以非同步回呼函式的形式來實現非阻塞的IO操作。
Javascript非同步程式設計帶來的挑戰
Javascript的單執行緒模型有很多好處,但同時也帶來了很多挑戰。
程式碼可讀性
想象一下,如果某個操作需要經過多個非阻塞的IO操作,每一個結果都是透過回呼,程式有可能會看上去像這個樣子。
operation1(function(err, result) {
operation2(function(err, result) {
operation3(function(err, result) {
operation4(function(err, result) {
operation5(function(err, result) {
// do something useful
})
})
})
})
})
我們稱之為義大利麵條式(spaghetti)的程式碼。這樣的程式碼很難維護。這樣的情況更多的會發生在server side的情況下。
流程控制
非同步帶來的另一個問題是流程控制,舉個例子,我要訪問三個網站的內容,當三個網站的內容都得到後,合併處理,然後發給後臺。程式碼可以這樣寫:
var urls = [‘url1′,’url2′,’url3’];
var result = [];
for (var i = 0, len = urls.length(); i < len; i++ ) {
$.ajax({
url: urls[i],
context: document.body,
success: function(){
//do something on success
result.push(“one of the request done successfully”);
if (result.length === urls.length()) {
//do something when all the request is completed successfully
}
}});
}
上述程式碼透過檢查result的長度的方式來決定是否所有的請求都處理完成,這是一個很醜陋方法,也很不可靠。
異常和錯誤處理
透過上一個例子,我們還可以看出,為了使程式更健壯,我們還需要加入異常處理。 在非同步的方式下,異常處理分佈在不同的回呼函式中,我們無法在呼叫的時候透過try…catch的方式來處理異常, 所以很難做到有效,清楚。
更好的Javascript非同步程式設計方式
“這是最好的時代,也是最糟糕的時代”
為瞭解決Javascript非同步程式設計帶來的問題,很多的開發者做出了不同程度的努力,提供了很多不同的解決方案。然而面對如此眾多的方案應該如何選擇呢?我們這就來看看都有哪些可供選擇的方案吧。
Promise
Promise 物件曾經以多種形式存在於很多語言中。這個詞最先由C++工程師用在Xanadu 專案中,Xanadu 專案是Web 應用專案的先驅。隨後Promise 被用在E程式語言中,這又激發了Python 開發人員的靈感,將它實現成了Twisted 框架的Deferred 物件。
2007 年,Promise 趕上了JavaScript 大潮,那時Dojo 框架剛從Twisted框架汲取靈感,新增了一個叫做dojo.Deferred 的物件。也就在那個時候,相對成熟的Dojo 框架與初出茅廬的jQuery 框架激烈地爭奪著人氣和名望。2009 年,Kris Zyp 有感於dojo.Deferred 的影響力提出了CommonJS 之Promises/A 規範。同年,Node.js 首次亮相。
在程式設計的概念中,future,promise,和delay表示同一個概念。Promise翻譯成中文是“承諾”,也就是說給你一個東西,我保證未來能夠做到,但現在什麼都沒有。它用來表示非同步操作傳回的一個物件,該物件是用來獲取未來的執行結果的一個代理,初始值不確定。許多語言都有對Promise的支援。
Promise的核心是它的then方法,我們可以使用這個方法從非同步操作中得到傳回值,或者是異常。then有兩個可選引數(有的實現是三個),分別處理成功和失敗的情景。
var promise = doSomethingAync()
promise.then(onFulfilled, onRejected)
非同步呼叫doSomethingAync傳回一個Promise物件promise,呼叫promise的then方法來處理成功和失敗。這看上去似乎並沒有很大的改進。仍然需要回呼。但是和以前的區別在於,首先非同步操作有了傳回值,雖然該值只是一個對未來的承諾;其次透過使用then,程式員可以有效的控制流程異常處理,決定如何使用這個來自未來的值。
對於巢狀的非同步操作,有了Promise的支援,可以寫成這樣的鏈式操作:
operation1().then(function (result1) {
return operation2(result1)
}).then(function (result2) {
return operation3(result2);
}).then(function (result3) {
return operation4(result3);
}).then(function (result4) {
return operation5(result4)
}).then(function (result5) {
//And so on
});
Promise提供更便捷的流程控制,例如Promise.all()可以解決需要併發的執行若干個非同步操作,等所有操作完成後進行處理。
var p1 = async1();
var p2 = async2();
var p3 = async3();
Promise.all([p1,p2,p3]).then(function(){
// do something when all three asychronized operation finished
});
對於異常處理,
doA()
.then(doB)
.then(null,function(error){
// error handling here
})
如果doA失敗,它的Promise會被拒絕,處理鏈上的下一個onRejected會被呼叫,在這個例子中就是匿名函式function(error){}。比起原始的回呼方式,不需要在每一步都對異常進行處理。這生了不少事。
以上只是對於Promise概念的簡單陳述,Promise擁有許多不同規範建議(A,A+,B,KISS,C,D等),名字(Future,Promise,Defer),和開源實現。大家可以參考一下的這些連結。
- jQuery’s Deferred Object
- YUI Promise Class
- Dojo Promises
- Q
- RSVP.js
- When.js
- MochiKit.Async
- FutureJS
- node-promise
- WinJS
如果你有選擇困難綜合症,面對這麼多的開源庫不知道如何決斷,先不要急,這還只是一部分,還有一些庫沒有或者不完全採用Promise的概念
Non-Promise
下麵列出了其它的一些開源的庫,也可以幫助解決Javascript中非同步程式設計所遇到的諸多問題,它們的解決方案各不相同,我這裡就不一一介紹了。大家有興趣可以去看看或者試用一下。
- Node-fibers
- Streamlinejs
- Step
- Flow-js
- Async
- Async.js
- slide-flow-control
- Non-3rd Party
其實,為瞭解決Javascript非同步程式設計帶來的問題,不一定非要使用Promise或者其它的開源庫,這些庫提供了很好的樣式,但是你也可以透過有針對性的設計來解決。
比如,對於層層回呼的樣式,可以利用訊息機制來改寫,假定你的系統中已經實現了訊息機制,你的code可以寫成這樣:
eventbus.on(“init”, function(){
operationA(function(err,result){
eventbus.dispatch(“ACompleted”);
});
});
eventbus.on(“ACompleted”, function(){
operationB(function(err,result){
eventbus.dispatch(“BCompleted”);
});
});
eventbus.on(“BCompleted”, function(){
operationC(function(err,result){
eventbus.dispatch(“CCompleted”);
});
});
eventbus.on(“CCompleted”, function(){
// do something when all operation completed
});
這樣我們就把巢狀的非同步呼叫,改寫成了順序執行的事件處理。
更多的方式,請大家參考這篇文章,它提出瞭解決非同步的五種樣式:回呼、觀察者樣式(事件)、訊息、Promise和有限狀態機(FSM)。
下一代Javscript對非同步程式設計的增強
ECMAScript6
下一代的Javascript標準Harmony,也就是ECMAScript6正在醞釀中,它提出了許多新的語言特性,比如箭頭函式、類(Class)、生成器(Generator)、Promise等等。其中Generator和Promise都可以被用於對非同步呼叫的增強。
Nodejs的開發版V0.11已經可以支援ES6的一些新的特性,使用node –harmony命令來執行對ES6的支援。
co、Thunk、Koa
koa是由Express原班人馬(主要是TJ)打造,希望提供一個更精簡健壯的nodejs框架。koa依賴ES6中的Generator等新特性,所以必須執行在相應的Nodejs版本上。
利用Generator、co、Thunk,可以在Koa中有效的解決Javascript非同步呼叫的各種問題。
co是一個非同步流程簡化的工具,它利用Generator把一層層巢狀的呼叫變成同步的寫法。
var co = require(‘co’);
var fs = require(‘fs’);
var stat = function(path) {
return function(cb){
fs.stat(path,cb);
}
};
var readFile = function(filename) {
return function(cb){
fs.readFile(filename,cb);
}
};
co(function *() {
var stat = yield stat(‘./README.md’);
var content = yield readFile(‘./README.md’);
})();
透過co可以把非同步的fs.readFile當成同步一樣呼叫,只需要把非同步函式fs.readFile用閉包的方式封裝。
利用Thunk可以進一步簡化為如下的code, 這裡Thunk的作用就是用閉包封裝非同步函式,傳回一個生成函式的函式,供生成器來呼叫。
var thunkify = require(‘thunkify’);
var co = require(‘co’);
var fs = require(‘fs’);
var stat = thunkify(fs.stat);
var readFile = thunkify(fs.readFile);
co(function *() {
var stat = yield stat(‘./README.md’);
var content = yield readFile(‘./README.md’);
})();
利用co可以序列或者並行的執行非同步呼叫。
序列
co(function *() {
var a = yield request(a);
var b = yield request(b);
})();
並行
co(function *() {
var res = yield [request(a), request(b)];
})();
更多詳細的內容,大家可以參考這兩篇文章1,2。
總結
非同步程式設計帶來的問題在客戶端Javascript中並不明顯,但隨著伺服器端Javascript越來越廣的被使用,大量的非同步IO操作使得該問題變得明顯。許多不同的方法都可以解決這個問題,本文討論了一些方法,但並不深入。大家需要根據自己的情況選擇一個適於自己的方法。
同時,隨著ES6的定義,Javascript的語法變得越來越豐富,更多的功能帶來了很多便利,然而原本簡潔,單一目的的Javascript變得複雜,也要承擔更多的任務。Javascript何去何從,讓我們拭目以待。
來自:Naughty的部落格
連結:http://my.oschina.net/taogang/blog/267707