你可能知道,javascript在瀏覽器端是一門單線程的語言(single thread)。道理很簡單,就是設(shè)計者當時設(shè)計這門語言的時候只是考慮到這門語言就是一個腳本語言,應該足夠簡單,要易于上手。

但是慢慢的發(fā)展就會產(chǎn)生一個問題,單線程一次只能執(zhí)行一個任務(wù),如果有多個任務(wù)同時執(zhí)行,就必須排隊,前面一個任務(wù)完成才能執(zhí)行后面一個任務(wù),以此類推。
這種模式下你不用考慮復雜的線程安全,也不用考慮線程通信,故出發(fā)點是好的。但是有一個壞處,會發(fā)生線程阻塞,就是一個耗時很長的任務(wù)如果沒有執(zhí)行完,就會阻塞之后任務(wù)的執(zhí)行。通俗的講就是會造成瀏覽器的假死
為了解決這個問題,Javascript語言將任務(wù)的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。
- "同步模式"就是上一段的模式,后一個任務(wù)等待前一個任務(wù)結(jié)束,然后再執(zhí)行,程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的;"異步模式"則完全不同,每一個任務(wù)有一個或多個回調(diào)函數(shù)(callback),前一個任務(wù)結(jié)束后,不是執(zhí)行后一個任務(wù),而是執(zhí)行回調(diào)函數(shù),后一個任務(wù)則是不等前一個任務(wù)結(jié)束就執(zhí)行,所以程序的執(zhí)行順序與任務(wù)的排列順序是不一致的、異步的。
-
"異步模式"非常重要。在瀏覽器端,耗時很長的操作都應該異步執(zhí)行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在服務(wù)器端,"異步模式"甚至是唯一的模式,因為執(zhí)行環(huán)境是單線程的,如果允許同步執(zhí)行所有http請求,服務(wù)器性能會急劇下降,很快就會失去響應。
本文總結(jié)了"異步模式"編程的4種方法,理解它們可以讓你寫出結(jié)構(gòu)更合理、性能更出色、維護更方便的Javascript程序。

提示:異步不代表加快執(zhí)行,通常我們說的異步是異步I/O,我們知道I/O操作遠沒有CPU的處理速度快。當我們?nèi)フ埱笠粋€HTTP服務(wù)、查詢一段SQL,讀取一個文件的時候,會出現(xiàn)CPU占用不完全的情況,這個時候CPU必須要等待I/O操作完成,這個等待時間是不定的,就會導致CPU資源浪費。
合理的解決辦法就是采用異步操作,當處于I/O等待狀態(tài)就將CPU轉(zhuǎn)換到其它任務(wù)當中,I/O結(jié)束等待后就重新獲得該方法的控制權(quán)繼續(xù)執(zhí)行。
回調(diào)函數(shù)
JavaScript語言對異步編程的實現(xiàn),就是回調(diào)函數(shù)。所謂回調(diào)函數(shù),就是把任務(wù)的第二段單獨寫在一個函數(shù)里面,等到重新執(zhí)行這個任務(wù)的時候,就直接調(diào)用這個函數(shù)。它的英語名字callback,直譯過來就是"重新調(diào)用"。
先看一個最簡單的回調(diào)
function f1(callback){
//setTimeout是最簡單的回調(diào)函數(shù),等待一段時間后執(zhí)行
setTimeout(function () {
// f1的任務(wù)代碼
callback();
}, 1000);
}
當我們執(zhí)行f1這個函數(shù)的時候,我們會加載一個定時器方法,1秒過后,會執(zhí)行它的回調(diào)函數(shù)
讀取文件進行處理,是這樣寫的。
fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});console.log(data);});
上面代碼讀取了一個文件,回調(diào)函數(shù)有兩個參數(shù),第一個為錯誤對象,第二個為讀取到的數(shù)據(jù)
讓情況稍微復雜一點
看下面這個代碼
$.post("/ajax/post1",function(data){
if(data.type=="book"){
$.post("/ajax/book",function(data1){
console.log(data1);
});
}else if(data.type=="article"){
$.post("/ajax/article",function(data2){
console.log(data2);
});
}
});
這個例子先請求了一個服務(wù),根據(jù)返回的結(jié)果,判斷它是哪一種狀態(tài),在根據(jù)它的狀態(tài)又請求了一次服務(wù)。這個例子看起來還比較好受,我們在看下面這種狀態(tài),是不是大家都很熟悉。
});
});
});
});
});
});
});
});
});
});
});
上面這段代碼就是javascript著名的回調(diào)陷阱,多次的嵌套回調(diào)會讓你的代碼亂成一團,并且難以維護
所以機智的開發(fā)人員為了解決這個問題,他們采取了很多種辦法,咱們從簡單的開始分析
異步函數(shù)式類庫
- Async.js
- When.js
- parallel.js
- ...
上面這些庫有一個共同點,就是通過函數(shù)的方式把異步方法嵌套在了一起,其實異步的本質(zhì)還是沒有改變,他們都基于Promises/A規(guī)范。
而且這些庫還有一個共通點,支持同步方法的隊列執(zhí)行,這個在js動畫、集合處理等用的比較多,這里不多闡述。
我們這里拿When.js來講解,首先,我們看一段代碼:
var getData = function(callback) {
$.getJSON(api, function(data){
callback(data[0]);
});
}
var getImg = function(src, callback) {
var img = new Image();
img.onload = function() {
callback(img);
};
img.src = src;
}
var showImg = function(img) {
$(img).appendTo($('#container'));
}
getData(function(data) {
getImg(data, function(img) {
showImg(img);
});
});
這段代碼完成了三個任務(wù):1)獲取數(shù)據(jù);2)加載圖片;3)顯示圖片,其中,任務(wù)1和2是異步,3是同步,使用的是最常見的callback機制來處理異步邏輯,好處是淺顯易懂,缺點是強耦合、不直觀、處理異常麻煩等等。
另外一種常見的實現(xiàn)異步編程的方案是事件監(jiān)聽器,例如使用QWrap的CustEvent,讓任務(wù)成功時fireEvent,那么注冊了這個Event的監(jiān)聽器就可以收到這個事件,并收到事件傳遞過來的數(shù)據(jù),Dom標準事件也是采用的這種形式。這種方案也很好理解,代碼從略。事件監(jiān)聽可以解耦,可以綁定任意多個監(jiān)聽器,但是依然不直觀,而且事件發(fā)生之后再綁定的監(jiān)聽器也得不到觸發(fā)。
我們嘗試用when.js改寫下這段代碼
var getData = function () {
var deferred = when.defer();
$.getJSON(api, function (data) {
deferred.resolve(data[0]);
});
return deferred.promise;
}
var getImg = function (src) {
var deferred = when.defer();
var img = new Image();
img.onload = function () {
deferred.resolve(img);
};
img.src = src;
return deferred.promise;
}
var showImg = function (img) {
$(img).appendTo($('#container'));
}
getData()
.then(getImg)
.then(showImg);
看最后三行代碼,是不是一目了然,非常的語義化?
他解決了一個問題,就是我們前面提到的嵌套陷阱!
OK!下一章讓我們來看一下Whenjs還有什么功能,當然這里的重點不是這個庫,大家感興趣的話可以自己去看看when.js,我們的重點是Promises/A規(guī)范
下一章我們繼續(xù)
Promises/A規(guī)范詳細闡述說明