《深入淺出Node.js》學習筆記——(四)非同步程式設計
Node能夠迅速成功並流行的原因:
①V8和非同步I/O帶來的效能提升
②前後端JavaScript程式設計風格一致
4.1 函數語言程式設計
4.1.1高階函式
可以將函式作為引數,或是作為返回值
4.1.2偏函式用法
指建立一個呼叫另外一個部分——引數或變數已經預置的函式——的函式的用法
4.2 非同步程式設計的優勢與難點
解決I/O效能的兩個方案:①多執行緒②通過C/C++呼叫作業系統底層介面
4.2.1優勢
Node的最大特性:基於事件驅動的非阻塞I/O
4.2.2難點
1.難點1:異常處理
非同步I/O實現的兩個階段:提交請求、處理結果
異常並不一定發生在提交請求階段,try/catch不會發生任何作用
非同步方法的定義:
varasync = function (callback) {
process.nextTick(callback);
};
對callback中的異常無能為力
varasync = function (callback) {
process.nextTick(callback);
};
解決方法:將異常作為回撥函式的第一個實參傳回,若為空值,則表明非同步呼叫沒有異常丟擲
自行編寫非同步方法需要遵循的原則:
①必須執行呼叫者傳入的回撥函式
②正確傳遞迴異常供呼叫者判斷
varasync = function (callback) {
process.nextTick(function(){
varresults = something;
if(error) {
returncallback(error);
}
callback(null,results);
});
};
非同步方法編寫中容易犯的錯誤:對使用者傳遞的回撥函式進行異常捕獲
try {
req.body= JSON.parse(buf, options.reviver);
callback();
} catch(err){
err.body= buf;
err.status= 400;
callback(err);
}
正確的捕獲:
try {
req.body= JSON.parse(buf, options.reviver);
} catch(err){
err.body= buf;
err.status= 400;
returncallback(err);
}
callback();
2.難點2:函式巢狀過深
3.難點3:阻塞程式碼
4.難點4:多執行緒程式設計
node借鑑了這個模式,child_process是其基礎API,cluster模組是更深層次的應用
5.難點5:非同步轉同步
通過良好的流程控制,將邏輯梳理成順序式的形式
4.3 非同步程式設計解決方案
非同步程式設計的主要解決方案:①事件釋出/訂閱模式②Promise/Deferred模式③流程控制庫
4.3.1事件釋出/訂閱模式
// 訂閱
emitter.on("event1",function (message) {
console.log(message);
});
// 釋出
emitter.emit('event1',"I am message!");
典型邏輯分離方式:通過事件釋出/訂閱模式進行元件封裝,將不變的部分封裝在元件內部,將容易變化、需自定義的部分通過事件暴露給外部處理。
元件中事件的設計即介面設計。
事件偵聽器模式也是一種鉤子機制,利用鉤子匯出內部資料或狀態給外部的呼叫者。程式設計者不用關注元件是如何啟動和執行的,只需關注在需要的事件點上即可。
Node基於健壯性對事件釋出/訂閱機制做的額外處理:
①對一事件新增超過10個偵聽器將會得到一條警告。防止記憶體洩漏和過多佔用CPU。
呼叫emitter.setMaxListeners(0)可以將這個限制去掉。
②執行期間的出錯時,eventemitter會檢查是否對error事件新增過偵聽器。添加了,則將錯誤交由偵聽器處理,否則作為異常丟擲。如果外部未捕獲該異常,將引起執行緒退出。
1.繼承events模組
varevents = require('events');
functionStream() {
events.EventEmitter.call(this);
}
util.inherits(Stream,events.EventEmitter);
2.利用事件佇列解決雪崩問題
Once()方法新增的偵聽器只能執行一次,之後與事件的關聯移除。
例:資料庫查詢語句呼叫,訪問量巨大
改進方案:①新增一個狀態鎖②使用once()方法
3.多非同步之間的協作方案
①偵聽器作為回撥函式可以隨意新增刪除,隨時新增業務邏輯
②也可以隔離業務邏輯,保持業務邏輯單元職責單一
一般事件與偵聽器的關係為一對多,在非同步程式設計中也會出現多對一的情況
需要藉助哨兵變數
4.EventProxy的原理
來自於Backbone的事件模組
每次非all事件觸發時觸發一次all事件
5.EventProxy的異常處理
Fail()/done() 事件釋出/訂閱模式向promise模式的借鑑
4.3.2Promise/Deferred模式
先執行非同步呼叫,延遲傳遞處理的方式
最早出現於dojo程式碼中,被廣為所知來自於jQuery1.5版本
1.Promises/A
①Promise操作的三種狀態:未完成態、完成態、失敗態
②Promise的狀態只會從未完成態向完成態或失敗態轉化,不能逆反。完成態和失敗態不能相互轉化
③Promise的狀態一旦轉化,將不能被更改
一個Promise物件只要具備then()方法即可
對於then()方法的簡單要求:
①接受完成態、錯誤態的回撥方法
②可選地支援progress事件回撥作為第三個方法
③then()方法只接受function物件,其餘物件將被忽略
④then()方法繼續返回promise物件,以實現鏈式呼叫
業務中不可變的部分封裝在Deferred中,可變的部分交給Promise
Promise是高階介面,事件是低階介面
低階介面可構成複雜場景,高階介面不靈活,用於解決典型問題。
2.Promise中的多非同步協作
簡單原型實現:
Deferred.prototype.all= function (promises) {
varcount = promises.length;
var that= this;
varresults = [];
promises.forEach(function(promise, i) {
promise.then(function(data) {
count--;
results[i]= data;
if(count === 0) {
that.resolve(results);
}
},function (err) {
that.reject(err);
});
});
returnthis.promise;
};
多次檔案讀取場景:
varpromise1 = readFile("foo.txt", "utf-8");
varpromise2 = readFile("bar.txt", "utf-8");
vardeferred = new Deferred();
deferred.all([promise1,promise2]).then(function (results) {
// TODO
},function (err) {
// TODO
});
3.Promise的進階知識
Promise的祕訣在於對佇列的操作
讓Promise支援鏈式執行需要通過的兩個步驟:
①將所有回撥存到佇列中
②Promise完成時,逐個執行回撥,一旦檢測到返回了新的Promise物件,停止執行,然後將當前Deferred物件的promise引用改變為新的Promise物件,並將佇列中餘下的回撥轉交給它
4.3.3流程控制庫
1.尾觸發與Next
尾觸發:需要手工呼叫才能持續執行後續呼叫
應用最多的地方是connect的中介軟體
Next()原理:取出佇列中的中介軟體並執行,同時傳入當前方法實現遞迴呼叫
並行邏輯處理需要搭配事件或者promise完成
connect中,尾觸發適合處理網路請求的場景,將複雜的處理邏輯拆解為簡潔、單一的處理單元,逐層次處理請求物件和響應物件
2.async
典型用法:
①非同步的序列執行
Series()實現任務序列執行
回撥函式由async通過高階函式注入,每個callback()執行將結果儲存起來,然後執行下一個呼叫,最終的回撥函式執行時,佇列裡非同步呼叫儲存的結果以陣列的方式傳入。
異常處理規則:一旦出現異常,結束所有呼叫,並將異常傳遞給最終回撥函式的第一個引數
②非同步的並行執行
Parallel()實現任務並行執行
③非同步呼叫的依賴處理
Waterfall()滿足當前結果是後一個呼叫的輸入的情況
④自動依賴處理
Auto()實現非同步、同步混雜的複雜業務處理
3.Step
比async更輕量
用到this關鍵字,是step內部的next()方法,將非同步呼叫的結果傳遞給下一個任務作為引數,並呼叫執行
①並行任務執行
This.parallel()
注意:如果非同步方法的結果傳回多個引數,step只取前兩個引數
step中parallel()原理:
每次執行將內部計數器加1,返回一個回撥函式,在非同步呼叫結束時執行,執行時計數器減1.計數器為0時,step執行下一個方法。
step與async異常處理相同
②結果分組
Group()方法,類似parallel(),結果傳遞略有不同
Parallel()傳遞給下一個任務的結果是如下形式:
Function(err,result1,result2,…)
Group()傳遞的結果是:
Function(err,results)
返回的資料儲存在陣列中
4.wind
完全不同的思路,基於任務模型實現,提高一些場景下的非同步程式設計體驗
如氣泡排序中的動畫效果
①非同步任務定義
Eval(wind.compile("async",function(){}));定義了非同步任務
Wind.async.sleep();內建了對setTimeout()的封裝
②$await()與任務模型
$await()是等待的佔位符,其引數是一個任務物件
whenAll() 通過$await關鍵字將等待配置的所有任務完成後繼續執行
③非同步方法轉換輔助函式
Wind.Async.Binding.fromCallback 用於轉換無異常的呼叫
Wind.Async.Binding.fromStandard 用於轉換帶異常的呼叫
5.流程控制小結
①事件釋出/訂閱模式是較為原始的方式,Promise/Deferred模式貢獻非同步任務模型的抽象
,其重頭在於封裝非同步的呼叫部分,流程控制庫則顯得沒有模式,處理重點在回撥函式的注入上。
②async\step等流控庫更靈活
③EventProxy庫主要借鑑事件釋出/訂閱模式和流程控制庫通過高階函式生成回撥函式的方式實現
除上述以外,還有一類通過原始碼編譯的方案實現流程控制的簡化,如streamline
4.4非同步併發控制
若對檔案系統進行大量併發呼叫,作業系統的檔案描述符數量會瞬間用光
過載保護方案
4.4.1 bagpipe的解決方案
①通過一個佇列來控制併發量
②若當前呼叫發起但未執行回撥的非同步呼叫量小於限定值,從佇列中取出執行
③如果活躍呼叫達到限定值,呼叫暫時放在佇列中
④每個非同步呼叫結束時,從佇列中取出新的非同步呼叫執行
Push()方法和full事件
Next()方法主要判斷活躍呼叫的數量,如果正常,呼叫內部方法run()來執行真正的呼叫
bagpipe允許非同步呼叫並行進行,但嚴格限定上限
僅僅在呼叫push()時分開傳遞,並不對原有API有任何入侵
1)拒絕模式
在呼叫有實時方面需求時,快速失敗,讓呼叫方儘早返回
2)超時控制
控制每個呼叫的執行時間,設定閾值
4.4.2 async的解決方案
parallelLimit(),與parallel()相比多了一個用於限制併發數量的引數
缺陷在於無法動態增加並行任務,queue()方法滿足該需求
4.5總結
NODE基於V8,目前還不支援協程(coroutine)