面試官:說說對 Node-js 中的事件循環機制理解?
一、是什麼
在瀏覽器事件循環中,我們瞭解到javascript
在瀏覽器中的事件循環機制,其是根據HTML5
定義的規範來實現
而在NodeJS
中,事件循環是基於libuv
實現,libuv
是一個多平臺的專注於異步 IO 的庫,如下圖最右側所示:
上圖EVENT_QUEUE
給人看起來只有一個隊列,但EventLoop
存在 6 個階段,每個階段都有對應的一個先進先出的回調隊列
二、流程
上節講到事件循環分成了六個階段,對應如下:
-
timers 階段:這個階段執行 timer(setTimeout、setInterval)的回調
-
定時器檢測階段 (timers):本階段執行 timer 的回調,即 setTimeout、setInterval 裏面的回調函數
-
I/O 事件回調階段 (I/O callbacks):執行延遲到下一個循環迭代的 I/O 回調,即上一輪循環中未被執行的一些 I/O 回調
-
閒置階段 (idle, prepare):僅系統內部使用
-
輪詢階段 (poll):檢索新的 I/O 事件; 執行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數,那些由計時器和 setImmediate() 調度的之外),其餘情況 node 將在適當的時候在此阻塞
-
檢查階段 (check):setImmediate() 回調函數在這裏執行
-
關閉事件回調階段 (close callback):一些關閉的回調函數,如:socket.on('close', ...)
每個階段對應一個隊列,當事件循環進入某個階段時, 將會在該階段內執行回調,直到隊列耗盡或者回調的最大數量已執行, 那麼將進入下一個處理階段
除了上述 6 個階段,還存在process.nextTick
,其不屬於事件循環的任何一個階段,它屬於該階段與下階段之間的過渡, 即本階段執行結束, 進入下一個階段前, 所要執行的回調,類似插隊
流程圖如下所示:
在Node
中,同樣存在宏任務和微任務,與瀏覽器中的事件循環相似
微任務對應有:
-
next tick queue:process.nextTick
-
other queue:Promise 的 then 回調、queueMicrotask
宏任務對應有:
-
timer queue:setTimeout、setInterval
-
poll queue:IO 事件
-
check queue:setImmediate
-
close queue:close 事件
其執行順序爲:
-
next tick microtask queue
-
other microtask queue
-
timer queue
-
poll queue
-
check queue
-
close queue
三、題目
通過上面的學習,下面開始看看題目
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
分析過程:
-
先找到同步任務,輸出 script start
-
遇到第一個 setTimeout,將裏面的回調函數放到 timer 隊列中
-
遇到第二個 setTimeout,300ms 後將裏面的回調函數放到 timer 隊列中
-
遇到第一個 setImmediate,將裏面的回調函數放到 check 隊列中
-
遇到第一個 nextTick,將其裏面的回調函數放到本輪同步任務執行完畢後執行
-
執行 async1 函數,輸出 async1 start
-
執行 async2 函數,輸出 async2,async2 後面的輸出 async1 end 進入微任務,等待下一輪的事件循環
-
遇到第二個,將其裏面的回調函數放到本輪同步任務執行完畢後執行
-
遇到 new Promise,執行裏面的立即執行函數,輸出 promise1、promise2
-
then 裏面的回調函數進入微任務隊列
-
遇到同步任務,輸出 script end
-
執行下一輪迴到函數,先依次輸出 nextTick 的函數,分別是 nextTick1、nextTick2
-
然後執行微任務隊列,依次輸出 async1 end、promise3
-
執行 timer 隊列,依次輸出 setTimeout0
-
接着執行 check 隊列,依次輸出 setImmediate
-
300ms 後,timer 隊列存在任務,執行輸出 setTimeout2
執行結果如下:
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2
最後有一道是關於setTimeout
與setImmediate
的輸出順序
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
輸出情況如下:
情況一:
setTimeout
setImmediate
情況二:
setImmediate
setTimeout
分析下流程:
-
外層同步代碼一次性全部執行完,遇到異步 API 就塞到對應的階段
-
遇到
setTimeout
,雖然設置的是 0 毫秒觸發,但實際上會被強制改成 1ms,時間到了然後塞入times
階段 -
遇到
setImmediate
塞入check
階段 -
同步代碼執行完畢,進入 Event Loop
-
先進入
times
階段,檢查當前時間過去了 1 毫秒沒有,如果過了 1 毫秒,滿足setTimeout
條件,執行回調,如果沒過 1 毫秒,跳過 -
跳過空的階段,進入 check 階段,執行
setImmediate
回調
這裏的關鍵在於這 1ms,如果同步代碼執行時間較長,進入Event Loop
的時候 1 毫秒已經過了,setTimeout
先執行,如果 1 毫秒還沒到,就先執行了setImmediate
參考文獻
-
https://segmentfault.com/a/1190000012258592
-
https://juejin.cn/post/6844904100195205133
-
https://vue3js.cn/interview/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7Mp-dfiT_ZcviqJkUKCS_Q