問答方式學 Node.js(二)
Q: 好了,我們繼續談談 Node.js 吧,我記得上次說到了「非阻塞」和「事件驅動」,這引起了我的好奇心,但是又給我潑了一桶冷水,這兩個詞有點高階。
A:別急,我們先來看一個簡單的場景:
人人網刷朋友新鮮事你應該用過吧?實現這樣的功能有一種簡單的方式,是讓使用者與伺服器之間保持一個長輪詢。
但是它與普通的 Ajax 不一樣,伺服器不會立馬返回資訊,它會先 hold 住,等待應該返回資訊了,它才會返回資訊(比如你的好友發了一條新的狀態)。
從傳統服務端來看(比如 Apache),每次一個新使用者連到你的網站上,你的伺服器得新開一個連線,每個連線都需要佔用一個執行緒,這些執行緒大部分都是閒著的(比如等你的好友髮狀態,查資料庫等),雖然它們是閒著的,但是照樣佔用了記憶體,也就是說,如果使用者達到一定的規模,伺服器的記憶體就會耗光而癱瘓。
解決辦法有很多,比如說使用執行緒池,但是它依然是阻塞的,如果執行緒池裡的所有執行緒都被阻塞(網速慢,被人惡意暫用)那麼接下來的請求將會排隊等待。
Node.js 就不相同了,它使用了「非阻塞」與「事件驅動」模型,你可以把它想象成一個 Event Loop 迴圈,這個迴圈會一直跑。一個新的請求來了,Event Loop 接收這個請求,然後交給其他執行緒,比如查詢資料庫,然後響應一個 callback,接著接收其他請求,而不是等待資料庫結果的返回。
如果資料庫返回了結果,服務端將會把它返回給客戶端,並繼續迴圈。這就是事件驅動:服務端只在有事情發生時,才會有相應的處理(或者是接受請求,或者是一些 callback)。
Q:這麼看來,Node.js 的非阻塞和事件驅動,是基於這個 Event Loop 的?
A:是的,簡單來講,Node.js 的 Event Loop 是基於 libuv,而瀏覽器的 Event Loop 則是在 html5 規範 中定義,具體實現交給瀏覽器廠商。
Q:有趣了,還有兩種 Event Loop。
A:對比來看,它們有點相似:
在瀏覽器中比較簡單,值得注意的一點是,會在每個 tasks 之後,會把當前 microtask 佇列裡的任務都執行完:
Node.js 稍微複雜一點,每次 Event Loop 都需要經過六個階段,每一個階段之後,都會執行 nextTick、microtasks (resolved promise, 等):
┌───────────────────────┐
┌─>│ timers │ <─── setTimeout/setInterval callback
│ └──────────┬────────────┘ ┌─────────────────────────┐
│ │ │ nextTick queue │
│ │ <─────────────── │ │
│ │ │ microTask queue │
│ ┌──────────┴────────────┐ └─────────────────────────┘
│ │ I/O callbacks │
│ └──────────┬────────────┘ ┌─────────────────────────┐
│ │ │ nextTick queue │
│ │ <─────────────── │ │
│ │ │ microTask queue │
│ ┌──────────┴────────────┐ └─────────────────────────┘
│ │ idle, prepare │ <─── 僅內部使用
│ └──────────┬────────────┘
│ │ ┌─────────────────────────┐
│ │ │ nextTick queue │
│ │ <──────────────── │ │
│ │ │ microTask queue │
│ │ └─────────────────────────┘
│ │ ┌─────────────────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │ <────┤ connections, │
│ └──────────┬────────────┘ │ data, etc │
│ │ └─────────────────────────┘
│ │ ┌─────────────────────────┐
│ │ │ nextTick queue │
│ │ <──────────────── │ │
│ │ │ microTask queue │
│ │ └─────────────────────────┘
│ ┌──────────┴────────────┐
│ │ check │ <─── setImmediate callback
│ └──────────┬────────────┘ ┌─────────────────────────┐
│ │ │ nextTick queue │
│ │ <─────────────── │ │
│ │ │ microTask queue │
│ ┌──────────┴────────────┐ └─────────────────────────┘
│ │ close callbacks │ <─── eg: socket.on("close",func)
│ └──────────┬────────────┘ ┌─────────────────────────┐
│ │ │ nextTick queue │
│ │ <─────────────── │ │
└─────────────┴ │ microTask queue │
└─────────────────────────┘
複製程式碼
來一段簡單的程式碼,猜猜瀏覽器(Chrome)和 Node.js 分別輸出什麼:
console.log('start');
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
console.log('end');
複製程式碼
Q:瀏覽器 (Chrome) 中肯定是輸出 start、end、time1、promise1、time2、promise2,至於 Node.js 中,我猜也是一樣的吧?
A:我們先來驗證一下:
瀏覽器中:
start
end
timer1
promise1
timer2
promise2
複製程式碼
Node.js 中:
start
end
timer1
timer2
promise1
promise2
複製程式碼
看來和想象中的不一樣,別急,看個動圖就會明白了:
瀏覽器中:
Node.js 中:
Q:原來是這樣,那如果在每個 setTimeout callback
里加上 process.nextTick
那麼是比 Promise.then
先執行?
A:是的,還記得上面所說過的嗎,在每個階段後都會執行 nextTick queue 以及 micktasks queue,nextTick queue 的優先順序比 micktasks queue 高。
Q:我懂了。對了,我記得你提到了 libuv,它是什麼?不是說 Node.js 使用的是 v8 嗎,它和 v8 又有什麼關係?
A:...
未完待續...