結(jié)論
┌───────────────────────┐
┌─>│ timers │<————— 執(zhí)行 setTimeout()察纯、setInterval() 的回調(diào)
│ └──────────┬────────────┘
| |<-- 執(zhí)行所有 Next Tick Queue 以及 MicroTask Queue 的回調(diào)
│ ┌──────────┴────────────┐
│ │ pending callbacks │<————— 執(zhí)行由上一個 Tick 延遲下來的 I/O 回調(diào)(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 執(zhí)行所有 Next Tick Queue 以及 MicroTask Queue 的回調(diào)
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 內(nèi)部調(diào)用(可忽略)
│ └──────────┬────────────┘
| |<-- 執(zhí)行所有 Next Tick Queue 以及 MicroTask Queue 的回調(diào)
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (執(zhí)行幾乎所有的回調(diào)空盼,除了 close callbacks 以及 timers 調(diào)度的回調(diào)和 setImmediate() 調(diào)度的回調(diào),在恰當?shù)臅r機將會阻塞在此階段)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 執(zhí)行所有 Next Tick Queue 以及 MicroTask Queue 的回調(diào)
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回調(diào)將會在這個階段執(zhí)行
│ └──────────┬────────────┘
| |<-- 執(zhí)行所有 Next Tick Queue 以及 MicroTask Queue 的回調(diào)
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on('close', ...)
└───────────────────────┘
對比瀏覽器
想理解整個 loop 的過程新荤,我們可以參照瀏覽器的 event loop揽趾,因為瀏覽器的比較簡單,如下:
┌───────────────────────┐
┌─>│ timers │<————— 執(zhí)行一個 MacroTask Queue 的回調(diào)
│ └──────────┬────────────┘
| |<-- 執(zhí)行所有 MicroTask Queue 的回調(diào)
| ────────────┘
是不是相比之下非常簡潔苛骨,就這么兩種 task queue篱瞎,簡單的一筆苟呐!
用一句話總結(jié)瀏覽器的 event loop 就是:
先執(zhí)行一個 MacroTask,然后執(zhí)行所有的 MicroTask俐筋;
再執(zhí)行一個 MacroTask牵素,然后執(zhí)行所有的 MicroTask;
……
如此反復澄者,無窮無盡……
注:可以把 script 標簽中的初始同步代碼視為一個初始的 MacroTask
解析
其實nodejs與瀏覽器的區(qū)別笆呆,就是nodejs的 MacroTask 分好幾種,而這好幾種又有不同的 task queue粱挡,而不同的 task queue 又有順序區(qū)別赠幕,而 MicroTask 是穿插在每一種【注意不是每一個!】MacroTask 之間的询筏。
其實圖中已經(jīng)畫的很明白:
setTimeout/setInterval 屬于 timers 類型榕堰;
setImmediate 屬于 check 類型;
socket 的 close 事件屬于 close callbacks 類型嫌套;
其他 MacroTask 都屬于 poll 類型逆屡。
process.nextTick 本質(zhì)上屬于 MicroTask,但是它先于所有其他 MicroTask 執(zhí)行踱讨;
所有 MicroTask 的執(zhí)行時機康二,是不同類型 MacroTask 切換的時候。
idle/prepare 僅供內(nèi)部調(diào)用勇蝙,我們可以忽略沫勿。
pending callbacks 不太常見,我們也可以忽略味混。
所以我們可以按照瀏覽器的經(jīng)驗得出一個結(jié)論:
先執(zhí)行所有類型為 timers 的 MacroTask产雹,然后執(zhí)行所有的 MicroTask(注意 NextTick 要優(yōu)先哦);
進入 poll 階段翁锡,執(zhí)行幾乎所有 MacroTask蔓挖,然后執(zhí)行所有的 MicroTask;
再執(zhí)行所有類型為 check 的 MacroTask馆衔,然后執(zhí)行所有的 MicroTask瘟判;
再執(zhí)行所有類型為 close callbacks 的 MacroTask,然后執(zhí)行所有的 MicroTask角溃;
至此拷获,完成一個 Tick,回到 timers 階段减细;
……
如此反復匆瓜,無窮無盡……
為了驗證這個結(jié)論,我們甚至可以舉一個例子:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
此代碼在瀏覽器環(huán)境會輸出什么呢?
timer1
promise1
timer2
promise2
但是 nodejs 會輸出:
timer1
timer2
promise1
promise2
如果你已經(jīng)理解了上面的現(xiàn)象驮吱,那我們已經(jīng)算基本了解 nodejs 的 event loop 了茧妒,但是其中還有一點細節(jié)
細節(jié)一:setTimeout 與 setImmediate 的順序
本來這不應該成為一個問題,因為在文首顯而易見左冬,timers 是在 check 之前的桐筏。
但事實上,Node 并不能保證 timers 在預設時間到了就會立即執(zhí)行拇砰,因為 Node 對 timers 的過期檢查不一定靠譜梅忌,它會受機器上其它運行程序影響,或者那個時間點主線程不空閑毕匀。比如下面的代碼,setTimeout() 和 setImmediate() 都寫在 Main 進程中癌别,但它們的執(zhí)行順序是不確定的:
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
雖然 setTimeout 延時為 0皂岔,但是一般情況 Node 把 0 會設置為 1ms,所以展姐,當 Node 準備 event loop 的時間大于 1ms 時躁垛,進入 timers 階段時,setTimeout 已經(jīng)到期圾笨,則會先執(zhí)行 setTimeout教馆;反之,若進入 timers 階段用時小于 1ms擂达,setTimeout 尚未到期土铺,則會錯過 timers 階段,先進入 check 階段板鬓,而先執(zhí)行 setImmediate
但有一種情況悲敷,它們兩者的順序是固定的:
const fs = require('fs')
fs.readFile('test.txt', () => {
console.log('readFile')
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
和之前情況的區(qū)別在于,此時 setTimeout 和 setImmediate 是寫在 I/O callbacks 中的俭令,這意味著后德,我們處于 poll 階段,然后是 check 階段抄腔,所以這時無論 setTimeout 到期多么迅速瓢湃,都會先執(zhí)行 setImmediate。本質(zhì)上是因為赫蛇,我們從 poll 階段開始執(zhí)行绵患,而非一個 Tick 的初始階段
細節(jié)二:poll 階段
poll 階段主要有兩個功能:
- 獲取新的 I/O 事件,并執(zhí)行這些 I/O 的回調(diào)悟耘,之后適當?shù)臈l件下 node 將阻塞在這里
- 當有 immediate 或已超時的 timers藏雏,執(zhí)行它們的回調(diào)
poll 階段用于獲取并執(zhí)行幾乎所有 I/O 事件回調(diào),是使得 node event loop 得以無限循環(huán)下去的重要階段。所以它的首要任務就是同步執(zhí)行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已執(zhí)行的 callbacks 達到一定上限掘殴,然后結(jié)束 poll 階段赚瘦,接下來會有幾種情況:
- setImmediate 的 queue 不為空,則進入 check 階段奏寨,然后是 close callbacks 階段……
- setImmediate 的 queue 為空起意,但是 timers 的 queue 不為空,則直接進入 timers 階段病瞳,然后又來到 poll 階段……
- setImmediate 的 queue 為空揽咕,timers 的 queue 也為空,此時會阻塞在這里套菜,因為無事可做亲善,也確實沒有循環(huán)下去的必要
細節(jié)三:關于 pending callbacks 階段
在很多文章中,將 pending callbacks 階段都寫作 I/O callbacks 階段逗柴,并說在此階段蛹头,執(zhí)行了除 close callbacks、 timers戏溺、setImmediate以外的幾乎所有的回調(diào)渣蜗,也就是把 poll 階段的工作與此階段的工作混淆了。
在我閱讀時旷祸,就曾產(chǎn)生過疑問耕拷,假如大部分回調(diào)是在 I/O callbacks 階段執(zhí)行的,那么 poll 階段就沒有理由阻塞托享,因為你并不能保證“無事可做”骚烧,你得去 I/O callbacks 階段檢查一下才知道嘛!
所以最終結(jié)合其他幾篇文章以及對源碼的分析闰围,應該可以確定止潘,I/O callbacks 更準確的叫做 pending callbacks,它所執(zhí)行的回調(diào)是比較特殊的辫诅、且不需要關心的凭戴,而真正重要的、大部分回調(diào)所執(zhí)行的階段是在 poll 階段炕矮。
關于 pending callbacks 有如下說法么夫,可以作為參考
查閱了libuv 的文檔后發(fā)現(xiàn),在 libuv 的 event loop 中肤视,
I/O callbacks
階段會執(zhí)行Pending callbacks
档痪。絕大多數(shù)情況下,在poll
階段邢滑,所有的 I/O 回調(diào)都已經(jīng)被執(zhí)行腐螟。但是,在某些情況下,有一些回調(diào)會被延遲到下一次循環(huán)執(zhí)行乐纸。也就是說衬廷,在I/O callbacks
階段執(zhí)行的回調(diào)函數(shù),是上一次事件循環(huán)中被延遲執(zhí)行的回調(diào)函數(shù)汽绢。
嚴格來說吗跋,i/o callbacks并不是處理文件i/o的callback 而是處理一些系統(tǒng)調(diào)用錯誤,比如網(wǎng)絡 stream, pipe, tcp, udp通信的錯誤callback宁昭。參考 因為跌宛,pending_queue的入列(queue_insert_tail)是通過一個叫 uv__io_feed 的api來調(diào)用的 而 uv__io_feed API是在tcp/udp/stream/pipe等相關API調(diào)用
注意,以上結(jié)論只針對node10及其之前版本积仗,如果是node11版本則和瀏覽器的eventloop相似(作者還沒仔細研究)
另外疆拘,node8之前的版本也可能因為底層使用libuv的不同而產(chǎn)生不穩(wěn)定的結(jié)果(坑爹啊)