1. 程式人生 > >《深入淺出Node.js》學習筆記——(四)非同步程式設計

《深入淺出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是其基礎APIcluster模組是更深層次的應用

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只取前兩個引數

stepparallel()原理:

每次執行將內部計數器加1,返回一個回撥函式,在非同步呼叫結束時執行,執行時計數器減1.計數器為0時,step執行下一個方法。

stepasync異常處理相同

②結果分組

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)