node端事件迴圈機制(Part2 - Timers、 Immediates 、 Next Ticks)
歡迎回來,在第一篇文章中, 描述了NodeJS事件迴圈的總體情況,在這一節中,將通過程式碼例項詳細討論三種重要的佇列。它們是, timers
, immediates
和 process.nextTick
的回掉.
文章指引
- Event Loop
- Timers、 Immediates 、 Next Ticks (本文)
- Promises、Next-Ticks、Immediates
- 處理 I/O
- 最佳的事件迴圈練習
- 在 Node v11中timers、microtasks發生的改變
I/O飢餓 例子一
這帶來了一個新問題。使用process遞迴/重複地向nextTick佇列新增事件。nextTick函式可能導致I/O和其他佇列永遠餓死。我們可以使用下面的簡單指令碼模擬這個場景。
const fs = require('fs'); function addNextTickRecurs(count) { let self = this; if (self.id === undefined) { self.id = 0; } if (self.id === count) return; process.nextTick(() => { console.log(`process.nextTick call ${++self.id}`); addNextTickRecurs.call(self, count); }); } addNextTickRecurs(Infinity); setTimeout(console.log.bind(console, 'omg! setTimeout was called'), 10); setImmediate(console.log.bind(console, 'omg! setImmediate also was called')); fs.readFile(__filename, () => { console.log('omg! file read complete callback was called!'); }); console.log('started');
您可以看到輸出是一個無限迴圈的nextTick回撥呼叫,以及 setTimeout、setImmediate 和fs.readFile
。從未呼叫readFile回撥.
started process.nextTick call 1 process.nextTick call 2 process.nextTick call 3 process.nextTick call 4 process.nextTick call 5 process.nextTick call 6 process.nextTick call 7 process.nextTick call 8 process.nextTick call 9 process.nextTick call 10 process.nextTick call 11 process.nextTick call 12 ....
例二
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0); setImmediate(() => { console.log('immediate') }) });
接下來我們來看一下它的執行流程
- 在開始時,這個程式使用
fs.readFile
非同步讀取當前檔案。它提供一個回撥函式,在讀取檔案後觸發。 - 然後事件迴圈開始。
- 一旦讀取檔案後,它將在事件迴圈的I/O佇列中新增事件(要執行的回撥)。
- 由於沒有要處理的其他事件,Node正在等待任何I/O事件。然後,它將在I/O佇列中看到檔案讀取事件並執行它。
- 在回撥的執行過程中, timer 被新增到 timer 堆中,並且 Immediates 被新增到 Immediates 佇列中
- 現在我們知道事件迴圈處於I/O階段。由於沒有要處理的任何I/O事件,因此事件迴圈將移動到immediate階段,然後立即執行 immediate 回撥。
- 在事件迴圈的下一個迴圈中,它將看到過期的 timer 並執行 timer 回撥。
例三
setImmediate(() => console.log('this is set immediate 1')); setImmediate(() => console.log('this is set immediate 2')); setImmediate(() => console.log('this is set immediate 3')); setTimeout(() => console.log('this is set timeout 1'), 0); setTimeout(() => { console.log('this is set timeout 2'); process.nextTick(() => console.log('this is process.nextTick added inside setTimeout')); }, 0); setTimeout(() => console.log('this is set timeout 3'), 0); setTimeout(() => console.log('this is set timeout 4'), 0); setTimeout(() => console.log('this is set timeout 5'), 0); process.nextTick(() => console.log('this is process.nextTick 1')); process.nextTick(() => { process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick')); }); process.nextTick(() => console.log('this is process.nextTick 2')); process.nextTick(() => console.log('this is process.nextTick 3')); process.nextTick(() => console.log('this is process.nextTick 4'));
-
首先會有如下的一些事件被新增到事件佇列中
- 3 immediates
- 5 timer 回掉
- 5 next tick 回掉
- 當事件迴圈開始時,它將注意到 next tick 佇列並開始處理 next tick 的回撥。在執行第二個 next tick 回撥期間,一個新的 next tick 回撥被新增到 next tick 佇列的末尾,並將在 next tick 佇列的末尾執行。
- 將執行過期 timer 的回撥。在第二個 timer 回撥函式的執行過程中,一個事件被新增到 next tick 佇列中。
- 一旦執行了所有過期 timer 的回撥,事件迴圈將看到下一個 timer 佇列中有一個事件(這是在執行第二個 timer 回撥期間新增的)。然後事件迴圈將執行它。
- 由於沒有要處理的I/O事件,因此事件迴圈將移動到即時佇列並處理即時佇列。
執行結果是下面這個樣子
this is process.nextTick 1 this is process.nextTick 2 this is process.nextTick 3 this is process.nextTick 4 this is the inner next tick inside next tick this is set timeout 1 this is set timeout 2 this is set timeout 3 this is set timeout 4 this is set timeout 5 this is process.nextTick added inside setTimeout this is set immediate 1 this is set immediate 2 this is set immediate 3