nodejs中的event loop

結(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
圖解瀏覽器 event loop 過程

但是 nodejs 會輸出:

timer1
timer2
promise1
promise2
圖解 nodejs event loop 過程

如果你已經(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 階段赚瘦,接下來會有幾種情況:

  1. setImmediate 的 queue 不為空,則進入 check 階段奏寨,然后是 close callbacks 階段……
  2. setImmediate 的 queue 為空起意,但是 timers 的 queue 不為空,則直接進入 timers 階段病瞳,然后又來到 poll 階段……
  3. 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)用

參考資料1
參考資料2

注意,以上結(jié)論只針對node10及其之前版本积仗,如果是node11版本則和瀏覽器的eventloop相似(作者還沒仔細研究)

另外疆拘,node8之前的版本也可能因為底層使用libuv的不同而產(chǎn)生不穩(wěn)定的結(jié)果(坑爹啊)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寂曹,一起剝皮案震驚了整個濱河市哎迄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌稀颁,老刑警劉巖芬失,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件楣黍,死亡現(xiàn)場離奇詭異匾灶,居然都是意外死亡,警方通過查閱死者的電腦和手機租漂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門阶女,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哩治,你說我怎么就攤上這事秃踩。” “怎么了业筏?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵憔杨,是天一觀的道長。 經(jīng)常有香客問我蒜胖,道長消别,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任台谢,我火速辦了婚禮寻狂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朋沮。我一直安慰自己蛇券,他們只是感情好,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纠亚,像睡著了一般塘慕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菜枷,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天苍糠,我揣著相機與錄音,去河邊找鬼啤誊。 笑死岳瞭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的蚊锹。 我是一名探鬼主播瞳筏,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牡昆!你這毒婦竟也來了姚炕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤丢烘,失蹤者是張志新(化名)和其女友劉穎柱宦,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體播瞳,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡掸刊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赢乓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忧侧。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖牌芋,靈堂內(nèi)的尸體忽然破棺而出蚓炬,到底是詐尸還是另有隱情,我是刑警寧澤躺屁,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布肯夏,位于F島的核電站,受9級特大地震影響犀暑,放射性物質(zhì)發(fā)生泄漏驯击。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一母怜、第九天 我趴在偏房一處隱蔽的房頂上張望余耽。 院中可真熱鬧,春花似錦苹熏、人聲如沸碟贾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袱耽。三九已至杀餐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朱巨,已是汗流浹背史翘。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留冀续,地道東北人琼讽。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像洪唐,于是被迫代替她去往敵國和親钻蹬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容