提前了解一下 Node 的 API 文檔,學(xué)習(xí)一下里面的方法是干什么用的,可以更好的理解書中舉例的一些方法,以防看到某個案例方法懵逼呦。好的,我們繼續(xù)。

異步I/O
現(xiàn)代的 Web 應(yīng)用已經(jīng)不再是單臺服務(wù)器就能勝任了,在跨網(wǎng)絡(luò)結(jié)構(gòu)下,并發(fā)已經(jīng)是現(xiàn)代編程的標(biāo)配了,所以異步 I/O 在 Node 里非常重要。
Node 完成整個異步 I/O 環(huán)節(jié)包括:
- 事件循環(huán)
- 觀察者
- 請求對象
事件循環(huán)
Node 的自身執(zhí)行模型就是事件循環(huán)。
在進程啟動時,Node 會創(chuàng)建一個類似 while(true)的循環(huán),每執(zhí)行一次循環(huán)循環(huán)體的過程我們稱為 Tick。每個 Tick 的過程就是查看是否有事件待處理,如果有,就取出事件及相關(guān)的回調(diào)函數(shù)。如果存在關(guān)聯(lián)的回調(diào)函數(shù),就執(zhí)行它們。然后進入下一個循環(huán),如果不再有事件處理,就退出進程。

觀察者
在每個 Tick 的過程中,判斷是否有事件需要處理的角色就稱為觀察者。
書里舉了一個很形象的例子:事件循環(huán)的過程就如同飯館的廚房,廚房一輪一輪的制作菜肴,但是要具體制作哪些菜肴取決于收銀臺收到的客人的下單。廚房每做完一輪菜,就去吻收銀臺的小妹,接下來還有沒有要做的菜,如果沒有的話,就下班打烊了。
這個過程中,收銀臺的小妹就是觀察者,他收到的客人點單就是關(guān)聯(lián)的回調(diào)函數(shù)。當(dāng)然,如果飯館經(jīng)營有方,它可能有多個收銀員,就如同事件循環(huán)中有多個觀察者一樣。收到下單就是一個事件,一個觀察者里可能有多個事件。

請求對象
這一節(jié)主要說的是從 JavaScript 代碼到系統(tǒng)內(nèi)核之間都發(fā)生了什么。
對于 Node 中的異步 I/O 調(diào)用而言,回調(diào)函數(shù)不由開發(fā)者調(diào)用。從 JavaScript 發(fā)起調(diào)用到內(nèi)核執(zhí)行完 I/O 操作的過渡過程中,存在一種中間產(chǎn)物,它就是請求對象。
以 fs.open()方法作為例子,探索 Node 與底層之間是如何執(zhí)行異步回調(diào)以及回調(diào)函數(shù)究竟如何被調(diào)用的:
fs.open = function(path,flags,mode,callback){
// ...
binding.open(pathModule._makeLong(path),stringToFlags(flags),mode,callback);
};
說實話,這里函數(shù)里面的代碼并不是很明白,書中說是 JavaScript 層面的代碼通過調(diào)用 C++ 核心模塊進行下層操作??赡苁抢锩娴拇a是內(nèi)建模塊編譯出來的,js 調(diào)用核心模塊。

JavaScript 調(diào)用 Node 的核心模塊,核心模塊調(diào)用 C++ 內(nèi)建模塊,內(nèi)建模塊進行系統(tǒng)調(diào)用,這是 Node 里的經(jīng)典調(diào)用。
從上圖可以看出fs.open()方法,其實是調(diào)用底層的uv_fs_open()方法,在調(diào)用這個方法的過程中,創(chuàng)建了一個請求對象,從 JavaScript 層面?zhèn)魅氲膮?shù)和當(dāng)前方法都被封裝在這個請求對象中,對象包裝完畢后,在 Windows 下,會將這個請求對象推入線程池(后邊會有解釋線程池)中等待執(zhí)行。
將請求對象推入線程池后,由 JavaScript 層面發(fā)起的異步調(diào)用的第一階段就結(jié)束了。JavaScript 線程就可以繼續(xù)執(zhí)行后邊的 JavaScript 操作了。當(dāng)前的 I/O 操作在線程池中等待執(zhí)行,就此達到異步的目的。
執(zhí)行回調(diào)
組裝好請求對象,送入 I/O 線程池等待執(zhí)行,實際上完成了異步 I/O 的第一部分,回調(diào)通知是第二部分。
線程池中的 I/O 操作調(diào)用完畢之后,會將結(jié)果存儲到 result 屬性上,然后告知當(dāng)前對象操作已完成,并將線程歸還線程池。
在這個過程中,其實還動用了事件循環(huán)的 I/O 觀察者。在每次 Tick 的執(zhí)行中,都會調(diào)用相關(guān)的方法檢查線程池中是否還有執(zhí)行完的的請求,有就將請求對象加入到 I/O 觀察者的隊列中,然后將其當(dāng)做事件處理。
I/O 觀察者回調(diào)函數(shù)的行為就是取出請求對象的 result 屬性作為參數(shù),取出里面的方法執(zhí)行,以此達到調(diào)用 JavaScript 中傳入的回調(diào)函數(shù)的目的。

從前面的異步 I/O 過程中,可以提取出異步 I/O 的幾個關(guān)鍵詞:單線程、事件循環(huán)、觀察者、I/O 線程池。
注意!這里的單線程和 I/O 線程池似乎是沖突的。其實:在 Node 中,除了 JavaScript 是單線程外,Node 自身是多線程的,只是 I/O 線程使用 CPU 較少。
另一個需要重視的觀點是:除了用戶代碼無法并行執(zhí)行外,所有的 I/O (磁盤 I/O 和網(wǎng)絡(luò) I/O 等)則是可以并行起來的。

事件驅(qū)動與高性能服務(wù)器
其實如果看懂了異步的實現(xiàn)原理,事件驅(qū)動這個概念,也應(yīng)該理解的差不多了,即通過主循環(huán)加事件觸發(fā)的方式來運行程序。
上面是利用讀取文件方法來解釋異步 I/O,其實異步 I/O 不僅僅應(yīng)用在文件操作中。在網(wǎng)絡(luò)請求層(Node 接收到網(wǎng)絡(luò),作為服務(wù)器),偵聽到的請求都會形成事件交給 I/O 觀察者。事件循環(huán)會不停地處理這些網(wǎng)絡(luò) I/O 事件。如果 JavaScript 有傳入回調(diào)函數(shù),這些事件將會最終傳遞到業(yè)務(wù)邏輯層進行處理。利用 Node 構(gòu)建 Web 服務(wù)器,正是在這樣的一個基礎(chǔ)上實現(xiàn)的。

幾種經(jīng)典的服務(wù)器模型,對比它們的優(yōu)缺點:
- 同步式 (一次只能處理一個請求,其余請求處于等待狀態(tài))
- 每進程/每請求(為每個請求啟動一個進程,這樣可以處理多個請求,但不具備擴展性,因為系統(tǒng)資源有限)
- 每線程/每請求(為每個請求啟動一個線程來處理。線程占內(nèi)存,大并發(fā)時內(nèi)存不足,服務(wù)器變緩慢)
Node 通過事件驅(qū)動方式處理請求,無需為每個請求創(chuàng)建額外線程,省掉創(chuàng)建和銷毀線程的開銷,同時系統(tǒng)調(diào)度任務(wù)時因為線程少,上下文切換代價也低。即使在大量并發(fā)時,也不受線程上下文切換開銷的影響,這是 Node 高性能的一個原因。
總結(jié)
1、異步 I/O 的關(guān)鍵詞:單線程、事件循環(huán)、觀察者、I/O 線程池。
2、在 Node 中,除了 JavaScript 是單線程外,Node 自身是多線程的,只是 I/O 線程使用 CPU 較少。
3、事件循環(huán)是異步實現(xiàn)的核心。
異步編程
有異步 I/O ,必有異步編程。
這一章主要講解的是高級函數(shù)的用法,異步編程的優(yōu)勢和難點,異步編程的解決方案和方案對應(yīng)的原理,異步并發(fā)控制的解決方案及原理。我沒有全部搞明白,只學(xué)習(xí)了一下常見的方法原理,精力有限。也可能是功力不夠,研究不動了 [允悲] 。有能力的兄臺可以自行查閱資料進行研究,也希望搞明白后可以指導(dǎo)指導(dǎo)。

函數(shù)式編程
熟悉 JavaScript 的前端開發(fā)者,肯定了解里面的高階函數(shù),說白了就是講函數(shù)作為參數(shù),或者返回值等操作。例如:
function fn(x){
return function(){
return x;
}
}
這種函數(shù)用法相信大部分前端工程師都有使用過的。
偏函數(shù)用法
偏函數(shù)用法是指:創(chuàng)建一個調(diào)用一個部分參數(shù)或變量已經(jīng)預(yù)置好的函數(shù)的函數(shù)用法。
我聽著也很拗口,意思就是:創(chuàng)建一個函數(shù) A,這個函數(shù) A 是用來調(diào)用另外一個函數(shù) B 的,函數(shù) B 的部分參數(shù)或變量是你定義好的,這種函數(shù) A 就叫偏函數(shù)(希望你聽懂了,哈哈)??蠢樱?/p>
var toString = Object.prototype.toString;
var isString = function(obj){ // 判斷對象是否為字符串
return toString.call(obj) == '[object String]';
};
var isFunction = function(obj){ // 判斷對象是否為函數(shù)
return toString.call(obj) == '[object Function]';
};
但是這種函數(shù)有一個問題,你想判斷幾種對象,就要寫幾個判斷的函數(shù),為了解決這個問題:
var isType = function(type){
return function(obj){
return toString.call(obj) == '[object '+type+']';
};
};
這種寫法就把你想判斷的類型寫活了。你想判斷什么類型就傳什么類型的 type ,這種形式就是偏函數(shù)。
異步編程的優(yōu)勢與難點
優(yōu)勢:
Node 帶來的最大特性莫過于基于事件驅(qū)動的非阻塞 I/O 模型,這也是它的靈魂所在。帶來的好處也是性能上的優(yōu)勢,讓資源得到更好的利用。對于網(wǎng)絡(luò)應(yīng)用而言,也備受青睞。


可以看出兩種模式在性能上的區(qū)別。
異步編程的難點主要有一下幾點:
- 異常處理
- 函數(shù)嵌套過深
- 阻塞代碼
- 多線程編程
- 異步轉(zhuǎn)同步
異步編程難點解決方案
針對上面的幾個難點,Node 也有專門的方案解決:
- 事件發(fā)布 / 訂閱模式(注冊 / 觸發(fā))
- Promise / Deferred 模式
- 流程控制庫
事件發(fā)布 / 訂閱模式:
這里講解的是 Node 的 events 模塊和一些相關(guān)的 API 方法的使用和原理,比如:addListener/on()(注冊方法),once()(注冊方法,只執(zhí)行一次),removeListener()(移除方法注冊),removeAllListeners()(移除所有注冊方法),emit()(觸發(fā)方法)。例如:
var events = require('events');
var emitter = new events.EventEmitter(); // 初始化
// 訂閱
emitter.on("event1",function(message){
console.log(message);
});
// 發(fā)布
emitter.emit("event1","This is message!");
Promise / Deferred 模式:
使用事件的方式時,執(zhí)行流程需要被預(yù)先設(shè)定。即便是分支,也需要預(yù)先設(shè)定,這是由發(fā)布 / 訂閱模式的運行機制所決定的。
這句話的意思是,你的異步函數(shù)里的選項必須齊全,不然就執(zhí)行不了。例如:
$.get('/url',{
success: onSuccess,
error: onError,
complete: onComplete
});
// 這個異步ajax,你不寫success項或error項就不行
Promise / Deferred 模式是一種先執(zhí)行異步調(diào)用,延遲傳遞處理方式的模式。例如:
$.get('/url')
.success(onSuccess)
.error(onError)
.complete(onComplete)
// 這種方式即使不調(diào)用success()等方法,ajax也會執(zhí)行。
流程控制庫
這里沒看太明白,記得后期補一補,只是知道各種類庫各顯神通。
事件發(fā)布 / 訂閱模式相對算是一種較為原始的方式,Promise / Deferred 模式貢獻了一個非常不錯的異步任務(wù)模型的抽象。流程控制庫方案與Promise / Deferred 模式不同,后者的重頭在于封裝異步的調(diào)用部分,前者將重點放在回調(diào)函數(shù)的注入上。
總結(jié)
異步編程是 Node 里比較難的一部分,就是在 JavaScript 中,高階函數(shù)也是個難點。
其實是因為人的線性思維慣性,對異步編程這種思維方式不太習(xí)慣,所以比較難學(xué),但是俗話說:世上無難事只怕有心人吶,相信經(jīng)過大量練習(xí)和學(xué)習(xí),這點是不難攻克的。

未完待續(xù)。。。。。。
文章只是本人學(xué)習(xí) Node 過程中,按自己的理解總結(jié)的一些筆記,若有錯誤之處,歡迎各位及時指出,一起探討更好的答案。
公眾號:前端很忙
做一個喜歡分享的前端開發(fā)者!
獲取更多干貨分享,歡迎來搞!