一直以來,我寫的的大部分JS程式碼都是在瀏覽器環境下執行,因此也瞭解過瀏覽器的事件迴圈機制,知道有macrotask和microtask的區別。但最近寫node時,發現node的事件迴圈機制和瀏覽器端有很大的不同,特此深入地學習了下。
單執行緒
在傳統web服務中,大多都是使用多執行緒機制來解決併發的問題,原因是I/O事件會阻塞執行緒,而阻塞就意味著要等待。而node的設計是採用了單執行緒的機制,但它為什麼還能承載高併發的請求呢?因為node的單執行緒僅針對主執行緒來說,即每個node程式只有一個主執行緒來執行程式程式碼,但node採用了事件驅動的機制,將耗時阻塞的I/O操作交給執行緒池中的某個執行緒去完成,主執行緒本身只負責不斷地排程,並沒有執行真正的I/O操作。也就是說node實現的是非同步非阻塞式。
事件迴圈機制
node能實現高併發的訣竅就在於事件迴圈機制,這個事件迴圈機制和瀏覽器端的相似但也有很多不同。根據node的官方介紹,node每次事件迴圈機制都包含了6個階段:
- timers階段:這個階段執行已經到期的timer(setTimeout、setInterval)回撥
- I/O callbacks階段:執行I/O(例如檔案、網路)的回撥
- idle, prepare 階段:node內部使用
- poll階段:獲取新的I/O事件, 適當的條件下node將阻塞在這裡
- check階段:執行setImmediate回撥
- close callbacks階段:執行close事件回撥,比如TCP斷開連線
對於日常開發來說,我們比較關注的是timers、I/O callbacks、check階段。node和瀏覽器相比一個明顯的不同就是node在每個階段結束後會去執行所有microtask任務。對於這個特點我們可以做個試驗:
console.log('main');
setImmediate(function() {
console.log('setImmediate');
});
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('promise.then');
});
複製程式碼
程式碼的執行結果是:
- main
- promise.then
- setImemediate
setImmediate 和 process.nextTick
相對於瀏覽器環境,node環境下多出了setImmediate和process.nextTick這兩種非同步操作。setImmediate的回撥函式是被放在check階段執行,即相當於事件迴圈的最後階段了。而process.nextTick會被當做一種microtask,前面提到每個階段結束後都會執行所有microtask任務,所以process.nextTick有種類似於插隊的作用,可以趕在下個階段前執行,但它和promise.then哪個先執行呢?通過一段程式碼來實驗:
console.log('main');
process.nextTick(function() {
console.log('nextTick')
})
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('promise.then');
});
複製程式碼
程式碼的執行結果是:
- main
- nextTick
- promise.then
事實證明,process.nextTick的優先順序會比promise.then高。
process.nextTick的飢餓陷阱
process.nextTick的優勢在於它能夠插入到每個階段之後,在當前階段執行完畢後就能立馬執行。然而它的這個優點也導致瞭如果呼叫不當就容易陷入飢餓陷阱。具體就是當遞迴地呼叫process.nextTick的時候,事件迴圈一直無法進入到下一個階段,導致了後面階段的事件一直無法被執行,產生飢餓問題。
看一個例子就很容易明白
let i = 0;
setImmediate(function() {
console.log('setImmediate');
});
function callback() {
console.log('nextTick' + i++);
if (i < 1000) {
process.nextTick(callback);
}
}
callback();
複製程式碼
執行的結果是 nextTick0 nextTick1 nextTick2 ... nextTick999 setImmediate
setImmediate的回撥會一直等待到process.nextTick任務都完成後才能被執行。
小結
1.node的事件迴圈機制和瀏覽器的有所不同,多出了setImmediate 和 process.nextTick這兩種非同步方式。由於process.nextTick會導致I/O飢餓,所以官方也推薦使用setImmediate。 2.node雖然是單執行緒的設計,但它也能實現高併發。原因在於它的主執行緒事件迴圈機制和底層執行緒池的實現。 3.這種機制決定了node比較適合I/O密集型應用,而不適合CPU密集型應用。