1. 程式人生 > 其它 >Node.js 中的事件迴圈機制

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

三、題目

通過上面的學習,下面開始看看題目

asyncfunctionasync1(){
console.log('async1start')
awaitasync2()
console.log('async1end')
}

asyncfunctionasync2(){
console.log('async2')
}

console.log('scriptstart')

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'));

newPromise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})

console.log('scriptend')

分析過程:

  • 先找到同步任務,輸出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

執行結果如下:

scriptstart
async1start
async2
promise1
promise2
scriptend
nextTick1
nextTick2
async1end
promise3
setTimeout0
setImmediate
setTimeout2

最後有一道是關於setTimeoutsetImmediate的輸出順序

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