Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西。
Node 的 Event Loop 分為 6 個階段,它們會按照順序反復運行。每當進入某一個階段的時候,都會從對應的回調(diào)隊列中取出函數(shù)去執(zhí)行。當隊列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達系統(tǒng)設(shè)定的閾值,就會進入下一階段。
(1)Timers(計時器階段):初次進入事件循環(huán),會從計時器階段開始。此階段會判斷是否存在過期的計時器回調(diào)(包含 setTimeout 和 setInterval),如果存在則會執(zhí)行所有過期的計時器回調(diào),執(zhí)行完畢后,如果回調(diào)中觸發(fā)了相應的微任務(wù),會接著執(zhí)行所有微任務(wù),執(zhí)行完微任務(wù)后再進入 Pending callbacks 階段。
(2)Pending callbacks:執(zhí)行推遲到下一個循環(huán)迭代的I / O回調(diào)(系統(tǒng)調(diào)用相關(guān)的回調(diào))。
(3)Idle/Prepare:僅供內(nèi)部使用。
(4)Poll(輪詢階段):
當回調(diào)隊列不為空時:會執(zhí)行回調(diào),若回調(diào)中觸發(fā)了相應的微任務(wù),這里的微任務(wù)執(zhí)行時機和其他地方有所不同,不會等到所有回調(diào)執(zhí)行完畢后才執(zhí)行,而是針對每一個回調(diào)執(zhí)行完畢后,就執(zhí)行相應微任務(wù)。執(zhí)行完所有的回調(diào)后,變?yōu)橄旅娴那闆r。
當回調(diào)隊列為空時(沒有回調(diào)或所有回調(diào)執(zhí)行完畢):但如果存在有計時器(setTimeout、setInterval和setImmediate)沒有執(zhí)行,會結(jié)束輪詢階段,進入 Check 階段。否則會阻塞并等待任何正在執(zhí)行的I/O操作完成,并馬上執(zhí)行相應的回調(diào),直到所有回調(diào)執(zhí)行完畢。
(5)Check(查詢階段):會檢查是否存在 setImmediate 相關(guān)的回調(diào),如果存在則執(zhí)行所有回調(diào),執(zhí)行完畢后,如果回調(diào)中觸發(fā)了相應的微任務(wù),會接著執(zhí)行所有微任務(wù),執(zhí)行完微任務(wù)后再進入 Close callbacks 階段。
(6)Close callbacks:執(zhí)行一些關(guān)閉回調(diào),比如socket.on('close', ...)等。
下面來看一個例子,首先在有些情況下,定時器的執(zhí)行順序其實是隨機的
對于以上代碼來說,setTimeout 可能執(zhí)行在前,也可能執(zhí)行在后首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的進入事件循環(huán)也是需要成本的,如果在準備時候花費了大于 1ms 的時間,那么在 timer 階段就會直接執(zhí)行 setTimeout 回調(diào)那么如果準備時間花費小于 1ms,那么就是 setImmediate 回調(diào)先執(zhí)行了當然在某些情況下,他們的執(zhí)行順序一定是固定的,比如以下代碼:
在上述代碼中,setImmediate 永遠先執(zhí)行。因為兩個代碼寫在 IO 回調(diào)中,IO 回調(diào)是在 poll 階段執(zhí)行,當回調(diào)執(zhí)行完畢后隊列為空,發(fā)現(xiàn)存在 setImmediate 回調(diào),所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了。
上面都是 macrotask 的執(zhí)行情況,對于 microtask 來說,它會在以上每個階段完成前清空 microtask 隊列,下圖中的 Tick 就代表了 microtask
對于以上代碼來說,其實和瀏覽器中的輸出是一樣的,microtask 永遠執(zhí)行在 macrotask 前面。最后來看 Node 中的 process.nextTick,這個函數(shù)其實是獨立于 Event Loop 之外的,它有一個自己的隊列,當每個階段完成后,如果存在 nextTick 隊列,就會清空隊列中的所有回調(diào)函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行。
對于以上代碼,永遠都是先把 nextTick 全部打印出來。