異步編程帶來的問題在客戶端Javascript中並不明顯,但隨着服務器端Javascript愈來愈廣的被使用,大量的異步IO操做使得該問題變得明顯。許多不一樣的方法均可以解決這個問題,本文討論了一些方法,但並不深刻。你們須要根據本身的狀況選擇一個適於本身的方法。javascript
筆者在以前的一片博客中簡單的討論了Python和Javascript的異同,其實做爲一種編程語言Javascript的異步編程是一個很是值得討論的有趣話題。html
所謂的異步指的是函數的調用並不直接返回執行的結果,而每每是經過回調函數異步的執行。java
咱們先看看回調函數是什麼:node
1
2
3
4
5
6
7
8
9
10
11
12
|
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通常也是使用回調對象。jquery
在Javascript中有不少經過回調函數來執行的異步調用,例如setTimeout()或者setInterval()。git
1
2
3
|
setTimeout(
function
(){
console.log(
"this will be exectued after 1 second!"
);
},1000);
|
在以上的例子中,setTimeout直接返回,匿名函數會在1000毫秒(不必定能保證是1000毫秒)後異步觸發並執行,完成打印控制檯的操做。也就是說在異步操做的情境下,函數直接返回,把控制權交給回調函數,回調函數會在之後的某一個時間片被調度執行。那麼爲何須要異步呢?爲何不能直接在當前函數中完成操做呢?這就須要瞭解Javascript的線程模型了。程序員
Javascript最初是被設計成在瀏覽器中輔助提供HTML的交互功能。在瀏覽器中都包含一個Javascript引擎,Javscript程序就運行在這個引擎之中,而且只有一個線程。單線程能都帶來不少優勢,程序員們能夠很開心的不用去考慮諸如資源同步,死鎖等多線程阻塞式編程所須要面對的惱人的問題。可是不少人會問,既然Javascript是單線程的,那它又如何可以異步的執行呢?es6
這就須要瞭解到Javascript在瀏覽器中的事件驅動(event driven)機制。事件驅動通常經過事件循環(event loop)和事件隊列(event queue)來實現的。假定瀏覽器中有一個專門用於事件調度的實例(該實例能夠是一個線程,咱們能夠稱之爲事件分發線程event dispatch thread),該實例的工做就是一個不結束的循環,從事件隊列中取出事件,處理全部很事件關聯的回調函數(event handler)。注意回調函數是在Javascript的主線程中運行的,而非事件分發線程中,以保證事件處理不會發生阻塞。github
Event Loop Code:web
1
2
3
4
5
6
7
8
|
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的單線程模型有不少好處,但同時也帶來了不少挑戰。
想象一下,若是某個操做須要通過多個非阻塞的IO操做,每個結果都是經過回調,程序有可能會看上去像這個樣子。
1
2
3
4
5
6
7
8
9
10
11
|
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的狀況下。
異步帶來的另外一個問題是流程控制,舉個例子,我要訪問三個網站的內容,當三個網站的內容都獲得後,合併處理,而後發給後臺。代碼能夠這樣寫:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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異步編程帶來的問題,不少的開發者作出了不一樣程度的努力,提供了不少不一樣的解決方案。然而面對如此衆多的方案應該如何選擇呢?咱們這就來看看都有哪些可供選擇的方案吧。
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有兩個可選參數(有的實現是三個),分別處理成功和失敗的情景。
1
2
|
var
promise = doSomethingAync()
promise.then(onFulfilled, onRejected)
|
異步調用doSomethingAync返回一個Promise對象promise,調用promise的then方法來處理成功和失敗。這看上去彷佛並無很大的改進。仍然須要回調。可是和之前的區別在於,首先異步操做有了返回值,雖然該值只是一個對將來的承諾;其次經過使用then,程序員能夠有效的控制流程異常處理,決定如何使用這個來自將來的值。
對於嵌套的異步操做,有了Promise的支持,能夠寫成這樣的鏈式操做:
1
2
3
4
5
6
7
8
9
10
11
|
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()能夠解決須要併發的執行若干個異步操做,等全部操做完成後進行處理。
1
2
3
4
5
6
|
var
p1 = async1();
var
p2 = async2();
var
p3 = async3();
Promise.all([p1,p2,p3]).then(
function
(){
// do something when all three asychronized operation finished
});
|
對於異常處理,
1
2
3
4
5
|
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),和開源實現。你們能夠參考一下的這些連接。
若是你有選擇困難綜合症,面對這麼多的開源庫不知道如何決斷,先不要急,這還只是一部分,還有一些庫沒有或者不徹底採用Promise的概念
下面列出了其它的一些開源的庫,也能夠幫助解決Javascript中異步編程所遇到的諸多問題,它們的解決方案各不相同,我這裏就不一一介紹了。你們有興趣能夠去看看或者試用一下。
其實,爲了解決Javascript異步編程帶來的問題,不必定非要使用Promise或者其它的開源庫,這些庫提供了很好的模式,可是你也能夠經過有針對性的設計來解決。
好比,對於層層回調的模式,能夠利用消息機制來改寫,假定你的系統中已經實現了消息機制,你的code能夠寫成這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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)。
下一代的Javascript標準Harmony,也就是ECMAScript6正在醞釀中,它提出了許多新的語言特性,好比箭頭函數、類(Class)、生成器(Generator)、Promise等等。其中Generator和Promise均可以被用於對異步調用的加強。
Nodejs的開發版V0.11已經能夠支持ES6的一些新的特性,使用node –harmony命令來運行對ES6的支持。
koa是由Express原班人馬(主要是TJ)打造,但願提供一個更精簡健壯的nodejs框架。koa依賴ES6中的Generator等新特性,因此必須運行在相應的Nodejs版本上。
利用Generator、co、Thunk,能夠在Koa中有效的解決Javascript異步調用的各類問題。
co是一個異步流程簡化的工具,它利用Generator把一層層嵌套的調用變成同步的寫法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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的做用就是用閉包封裝異步函數,返回一個生成函數的函數,供生成器來調用。
1
2
3
4
5
6
7
8
9
10
11
|
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能夠串行或者並行的執行異步調用。
串行
1
2
3
4
|
co(
function
*() {
var
a = yield request(a);
var
b = yield request(b);
})();
|
並行
1
2
3
|
co(
function
*() {
var
res = yield [request(a), request(b)];
})();
|
異步編程帶來的問題在客戶端Javascript中並不明顯,但隨着服務器端Javascript愈來愈廣的被使用,大量的異步IO操做使得該問題變得明顯。許多不一樣的方法均可以解決這個問題,本文討論了一些方法,但並不深刻。你們須要根據本身的狀況選擇一個適於本身的方法。
同時,隨着ES6的定義,Javascript的語法變得愈來愈豐富,更多的功能帶來了不少便利,然而本來簡潔,單一目的的Javascript變得複雜,也要承擔更多的任務。Javascript何去何從,讓咱們拭目以待。