node中的Event Loop

laihuamin發表於2019-02-27

關於事件這一塊在《深入淺出的nodejs》中很少講到,書裡面只是在第三章提及了4個API方法,比如兩個定時器(setTimeout和setInterval),process.nextTick()和setImmediate。

瀏覽器中的event loop

前篇回顧

setTimeout(()=>{
    console.log(`timer1`)

    Promise.resolve().then(function() {
        console.log(`promise1`)
    })
}, 0)

setTimeout(()=>{
    console.log(`timer2`)

    Promise.resolve().then(function() {
        console.log(`promise2`)
    })
    setTimeout(() => {
    	console.log(`timer3`)
    }, 0)
}, 0)

Promise.resolve().then(function() {
    console.log(`promise3`)
})

console.log(`start`)
複製程式碼

瀏覽器中輸出結果:

start
promise3
timer1
promise1
timer2
promise2
timer3
複製程式碼

這個輸出結果的原因我們已經在上一篇文章中說明,本章就不多加贅述。

在nodejs中,執行卻能得到不同的結果,讓我們先來過一下node的事件模型。

start
promise3
timer1
timer2
promise1
promise2
timer3
複製程式碼

node中的事件迴圈模型

node的事件迴圈分為6個階段

原始碼地址有興趣的可以去看一下原始碼

node-phase.png

六個階段的功能如下:

  • timers:這個階段執行定時器佇列中的回撥,比如setTimeout()和setInterval()
  • I/O callbacks:這個階段執行幾乎所有的回撥。但是不包括close事件,定時器和setImmediate的回撥。
  • idle,prepare:僅在內部使用。
  • poll:等待新的I/O事件,node會在一些特殊的情況下使用
  • check:setImmediate()的回撥會在這個中執行。
  • close callbacks:例如socket.on(`close`, …)執行close的回撥。

poll階段

當有資料或者連線傳入事件迴圈的時候,先進入的是poll階段,這個階段,先檢查poll queue中是否有事件,有任務就按先進先出的順序執行回撥,如果佇列為空,那麼會先檢查是否有到期的setImmdiate,如果有,將其的callback推入check佇列中,同時還會檢查是否有到期的timer,如果有,將其callback推入到timers佇列中。如果前面兩者都為空,那麼直接進入I/O callback,並執行這個事件的callback。

check階段

check階段專門用來執行setImmidate的回撥函式。

close階段

用於執行close事件的回撥函式

timer階段

用於執行定時器設定的回撥函式

I/O callback階段

用於執行大部分I/O事件的回撥函式。

process.nextTick

這個鉤子在node的事件迴圈模型中沒有提及,但是node中有一個特殊的佇列,nextTick queue。在node事件迴圈進入到下一個階段的時候,都會去檢測nextTick queue中有沒有清空,如果沒有清空,那麼就會去清空nextTick queue中的事件。

迴圈過程

官網的文件裡面有那麼一段話:

When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

  • 迴圈開始之前

1、所有同步任務
2、指令碼任務中傳送的api請求
3、規劃定時器同步任務的生效時間
4、執行process.nextTick()

  • 開始迴圈

第一種情況

1、清空當前迴圈內的 Timers Queue,清空NextTick Queue,清空Microtask Queue
2、清空當前迴圈內的 I/O Queue,清空NextTick Queue,清空Microtask Queue
3、進入poll階段
4、清空當前迴圈內的 Check Queue,清空NextTick Queue,清空Microtask Queue
5、清空當前迴圈內的 Close Queue,清空NextTick Queue,清空Microtask Queue

第二種情況

1、先進入poll階段
2、清空當前迴圈內的 Check Queue,清空NextTick Queue,清空Microtask Queue
3、清空當前迴圈內的 Close Queue,清空NextTick Queue,清空Microtask Queue
4、清空當前迴圈內的 Timers Queue,清空NextTick Queue,清空Microtask Queue
5、清空當前迴圈內的 I/O Queue,清空NextTick Queue,清空Microtask Queue

  • setTimeout 和 setImmediate 的區別
setTimeout(() => {
  console.log(`timeout`)
}, 0);

setImmediate(() => {
  console.log(`immediate`)
});
複製程式碼

直接執行指令碼,輸出的結果是

timeout
immediate
複製程式碼

當我們把他放在同一個I/O迴圈中執行

const fs = require(`fs`);

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log(`timeout`)
    }, 0)
    setImmediate(() => {
        console.log(`immediate`)
    })
})
複製程式碼

輸出的結果是

immediate
timeout
複製程式碼
  • process.nextTick和microtask
process.nextTick(() => {
    console.log(`nextTick`)
})

Promise.resolve().then(() => {
    console.log(`promise`)
})
複製程式碼

輸出的結果是

nextTick
promise
複製程式碼

nodejs中的實現方式:microtask queue的任務通過runMicrotasks將microtask queue中的task放入到nextTick中,所以microtask的任務會在nextTick queue之後執行。

  • process.nextTick() 和 setImmediate()

書本中是推薦使用setImmediate(),使用者如果遞迴呼叫process.nextTick()的時候,會造成I/O被榨乾。而使用setImmediate,只會在check中執行,不至於非同步呼叫的時候無法執行。

相關文章