目錄
- 微任務
- 事件迴圈機制
- setImmediate、setTimeout/setInterval 和 process.nextTick 執行時機對比
- 例項分析
- 參考
1.微任務
在談論Node的事件迴圈機制之前,先補充說明一下 Node 中的“微任務”。這裡說的微任務(microtasks)其實是一個統稱,包含了兩部分:
- process.nextTick() 註冊的回撥 (nextTick task queue)
- promise.then() 註冊的回撥 (promise task queue)
Node 在執行微任務時, 會優先執行 nextTick task queue 中的任務,執行完之後會接著執行 promise task queue 中的任務。所以如果 process.nextTick 的回撥與 promise.then 的回撥都處於主執行緒或事件迴圈中的同一階段, process.nextTick 的回撥要優先於 promise.then 的回撥執行。
2.事件迴圈機制
如圖,表示Node執行的整個過程。如果執行了任何非阻塞非同步程式碼(建立計時器、讀寫檔案等),則會進入事件迴圈。其中事件迴圈分為六個階段:
由於Pending callbacks、Idle/Prepare 和 Close callbacks 階段是 Node 內部使用的三個階段,所以這裡主要分析與開發者程式碼執行更為直接關聯的Timers、Poll 和 Check 三個階段。
Timers(計時器階段):從圖可見,初次進入事件迴圈,會從計時器階段開始。此階段會判斷是否存在過期的計時器回撥(包含 setTimeout 和 setInterval),如果存在則會執行所有過期的計時器回撥,執行完畢後,如果回撥中觸發了相應的微任務,會接著執行所有微任務,執行完微任務後再進入 Pending callbacks 階段。
Pending callbacks:執行推遲到下一個迴圈迭代的I / O回撥(系統呼叫相關的回撥)。
Idle/Prepare:僅供內部使用。(詳略)
Poll(輪詢階段):
當回撥佇列不為空時:
會執行回撥,若回撥中觸發了相應的微任務,這裡的微任務執行時機和其他地方有所不同,不會等到所有回撥執行完畢後才執行,而是針對每一個回撥執行完畢後,就執行相應微任務。執行完所有的回到後,變為下面的情況。
當回撥佇列為空時(沒有回撥或所有回撥執行完畢):
但如果存在有計時器(setTimeout、setInterval和setImmediate)沒有執行,會結束輪詢階段,進入 Check 階段。否則會阻塞並等待任何正在執行的I/O操作完成,並馬上執行相應的回撥,直到所有回撥執行完畢。
Check(查詢階段):會檢查是否存在 setImmediate 相關的回撥,如果存在則執行所有回撥,執行完畢後,如果回撥中觸發了相應的微任務,會接著執行所有微任務,執行完微任務後再進入 Close callbacks 階段。
Close callbacks:執行一些關閉回撥,比如 socket.on('close', ...)
等。
總結&注意:
- 每一個階段都會有一個FIFO回撥佇列,都會盡可能的執行完當前階段中所有的回撥或到達了系統相關限制,才會進入下一個階段。
- Poll 階段執行的微任務的時機和 Timers 階段 & Check 階段的時機不一樣,前者是在每一個回撥執行就會執行相應微任務,而後者是會在所有回撥執行完之後,才統一執行相應微任務。
3.setImmediate、setTimeout/setInterval 和 process.nextTick 執行時機對比
setImmediate:觸發一個非同步回撥,在事件迴圈的 Check 階段立即執行。
setTimeout:觸發一個非同步回撥,當計時器過期後,在事件迴圈的 Timers 階段執行,只執行一次(可用 clearTimeout 取消)。
setInterval:觸發一個非同步回撥,每次計時器過期後,都會在事件迴圈的 Timers 階段執行一次回撥(可用 clearInterval 取消)。
process.nextTick:觸發一個微任務(非同步)回撥,既可以在主執行緒(mainline)中執行,可以存在事件循序的某一個階段中執行。
4.例項分析
第一組:
比較 setTimeout 與 setImmediate:
// test.js
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
結果:
分析:
從輸出結果來看,輸出是不確定的,既可能 "setTimeout" 在前,也可能 "setImmediate" 在前。從事件迴圈的流程來分析,事件迴圈開始,會先進入 Timers 階段,雖然 setTimeout 設定的 delay 是 0,但其實是1,因為 Node 中的 setTimeout 的 delay 取值範圍必須是在 [1, 2^31-1] 這個範圍內,否則預設為1,因此受程式效能的約束,執行到 Timers 階段時,可能計時器還沒有過期,所以繼續向下一個流程進行,所以會偶爾出現 "setImmediate" 輸出在前的情況。如果適當地調大 setTimeout 的 delay,比如10,則基本上必然是 "setImmediate" 輸出在前面。
第二組:
比較主執行緒(mainline)、Timers 階段、Poll 階段和 Check 階段的回撥執行以及對應的微任務執行的順序:
// test.js
const fs = require('fs');
console.log('mainline: start')
process.nextTick(() => {
console.log('mainline: ', 'process.nextTick\n')
})
let counter = 0;
const interval = setInterval(() => {
console.log('timers: setInterval.start ', counter)
if(counter < 2) {
setTimeout(() => {
console.log('timers: setInterval.setTimeout')
process.nextTick(() => {
console.log('timers microtasks: ', 'setInterval.setTimeout.process.nextTick\n')
})
}, 0)
fs.readdir('./', (err, files) => {
console.log('poll: setInterval.readdir1')
process.nextTick(() => {
console.log('poll microtasks: ', 'setInterval.readdir1.process.nextTick')
process.nextTick(() => {
console.log('poll microtasks: ', 'setInterval.readdir1.process.nextTick.process.nextTick')
})
})
})
fs.readdir('./', (err, files) => {
console.log('poll: setInterval.readdir2')
process.nextTick(() => {
console.log('poll microtasks: ', 'setInterval.readdir2.process.nextTick')
process.nextTick(() => {
console.log('poll microtasks: ', 'setInterval.readdir2.process.nextTick.process.nextTick\n')
})
})
})
setImmediate(() => {
console.log('check: setInterval.setImmediate1')
process.nextTick(() => {
console.log('check microtasks: ', 'setInterval.setImmediate1.process.nextTick')
})
})
setImmediate(() => {
console.log('check: setInterval.setImmediate2')
process.nextTick(() => {
console.log('check microtasks: ', 'setInterval.setImmediate2.process.nextTick\n')
})
})
} else {
console.log('timers: setInterval.clearInterval')
clearInterval(interval)
}
console.log('timers: setInterval.end ', counter)
counter++;
}, 0);
console.log('mainline: end')
結果:
分析:
如圖 mainline:可以看到,主執行緒中的 process.nextTick 是在同步程式碼執行完之後以及在事件迴圈之前執行,符合預期。
如圖 第一次 timers:此時事件迴圈第一次到 Timers 階段,setInterval 的 delay 時間到了,所以執行回撥,由於沒有觸發直接相應的微任務,所以直接進入後面的階段。
如圖 第一次 poll:此時事件迴圈第一次到 Poll 階段,由於之前 Timers 階段執行的回撥中,觸發了兩個非阻塞的I/O操作(readdir),在這一階段時I/O操作執行完畢,直接執行了對應的兩個回撥。從輸出可以看出,針對每一個回撥執行完畢後,就執行相應微任務,微任務中再次觸發微任務也會繼續執行,並不會等到所有回撥執行完後再去觸發微任務,符合預期。執行完畢所有回撥之後,因為還有排程了計時器,所以 Poll 階段結束,進入 Check 階段。
如圖 第一次 check:此時事件迴圈第一次到 Check 階段,直接觸發對應的兩個 setImmediate 執行。從輸出可以看出,微任務是在所有的回撥執行完畢之後才觸發執行的,符合預期。執行完微任務後,進入後面階段。
如圖 第二次 timers:此時事件迴圈第二次到 Timers 階段,首先輸出了 "timers: setInterval.setTimeout" ,這是為什麼?不要忘了,之前第一次執行 setInterval 的回撥時,其實已經執行了一次其內部的 setTimeout(..., 0),但由於它並不能觸發微任務,所以其回撥沒有被執行,而是進入到了後面的階段,而是等到再次來到 Timers 階段,根據FIFO,優先執行之前的 setTimeout 的回撥,再執行 setInterval 的回撥,而最後等所有回撥執行完畢,再執行 setTimeout 的回撥裡面觸發的微任務,最後輸出的是 "timers microtasks: setInterval.setTimeout.process.nextTick",符合預期(所有回撥執行完畢後,再執行相應微任務)。
後面的輸出類似,所以不再做過多分析。
5.參考