NodeJs 的 Event loop 事件循環機制詳解


什么是事件輪詢

事件循環是 Node.js 處理非阻塞 I/O 操作的機制——盡管 JavaScript 是單線程處理的——當有可能的時候,它們會把操作轉移到系統內核中去。

下面的圖表顯示了事件循環的概述以及操作順序。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     IO / callbacks        │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

三大關鍵階段

  1. timer:執行定時器時,如 setTimeout、setInterval,在 timers 階段執行
  2. poll:異步操作,比如文件I/O,網絡I/O等,通過'data'、 'connect'等事件通知 JS 主線程並執行回調的,此階段就是 poll 輪詢階段
  3. check:這是一個比較簡單的階段,直接執行 setImmdiate 的回調。

注意,若 2 階段結束后,當前存在到時間的定時器,那么拿出來執行,eventLoop 將再回到 timer 階段

階段流程概述

  • timers: 本階段執行已經安排的 setTimeout() 和 setInterval() 的回調函數
  • IO / callbacks: 執行 I/O 異常的回調,如TCP 連接遇到 ECONNREFUSED
  • idle, prepare: 僅系統內部使用,只是表達空閑、預備狀態(第2階段結束,poll 未觸發之前)
  • poll: 檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數),node 將在此處阻塞。
  • check: setImmediate() 回調函數在這里執行.
  • close callbacks: 一些准備關閉的回調函數,如:socket.on('close', ...)

在每次運行的事件循環之間,Node.js 檢查它是否在等待任何異步 I/O 或計時器,如果沒有的話,則關閉干凈。

timers

timers 指定 可執行所提供回調 的 時間閾值,poll 階段 控制何時定時器執行。

一旦 poll queue 為空,事件循環將檢查 已達到時間閾值的timer計時器。如果一個或多個計時器已准備就緒,則事件循環將回到 timer 階段以執行這些計時器的回調

pending callbacks

此階段對某些系統操作(如 TCP 錯誤類型)執行回調。例如,如果 TCP 套接字在嘗試連接時接收到 ECONNREFUSED,則某些 *nix 的系統希望等待報告錯誤。這將被排隊以在 pending callbacks 階段執行。

poll

輪詢 階段有兩個重要的功能:

  • 計算應該阻塞和 poll I/O 的時間。
  • 然后,處理 poll 隊列里的事件。

當事件循環進入 poll階段且 timers scheduled,將發生以下兩種情況之一:

  • if the poll queue is not empty, 事件循環將循環訪問其回調隊列並同步執行它們,直到隊列已用盡,或者達到了與系統相關的硬限制
  • If the poll queue is empty,還有兩件事發生
    • 如果腳本已按 setImmediate() 排定,則事件循環將結束 輪詢 階段,並繼續 檢查 階段以執行這些計划腳本。
    • 如果腳本尚未按 setImmediate()排定,則事件循環將等待回調添加到隊列中,然后立即執行。

一旦 poll queue 為空,事件循環將檢查 已達到時間閾值的timer計時器。如果一個或多個計時器已准備就緒,則事件循環將回到 timer 階段以執行這些計時器的回調。

check

通常,在執行代碼時,事件循環最終會命中輪詢階段,等待傳入連接、請求等。但是,如果回調已計划為 setImmediate(),並且輪詢階段變為空閑狀態,則它將結束並繼續到檢查階段而不是等待輪詢事件。

setImmediate() 實際上是一個在事件循環的單獨階段運行的特殊計時器。它使用一個 libuv API 來安排回調在 poll 階段完成后執行。

close callbacks

如果套接字或處理函數突然關閉(例如 socket.destroy()),則'close' 事件將在這個階段發出。否則它將通過 process.nextTick() 發出。

setImmediate() 對比 setTimeout()

setImmediate() 和 setTimeout() 很類似,但何時調用行為完全不同。

  • setImmediate() 設計為在當前 輪詢 階段完成后執行腳本。
  • setTimeout() 計划在毫秒的最小閾值經過后運行的腳本。

執行計時器的順序將根據調用它們的上下文而異,如果二者都從主模塊內調用,則計時將受進程性能的約束,兩個計時器的順序是非確定性的。

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

但是,如果你把這兩個函數放入一個 I/O 循環內調用,setImmediate 總是被優先調用:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

使用 setImmediate() 超過 setTimeout() 的主要優點是 setImmediate() 在任何計時器(如果在 I/O 周期內)都將始終執行,而不依賴於存在多少個計時器。

process.nextTick()

process.nextTick() 在技術上不是事件循環的一部分,無論事件循環的當前階段如何,都將在當前操作完成后處理 nextTickQueue。這里的一個操作被視作為一個從 C++ 底層處理開始過渡,並且處理需要執行的 JavaScript 代碼。

回顧我們的關系圖,任何時候在給定的階段中調用 process.nextTick(),所有傳遞到 process.nextTick() 的回調將在事件循環繼續之前得到解決。這可能會造成一些糟糕的情況, 因為它允許您通過進行遞歸 process.nextTick() 來“餓死”您的 I/O 調用,阻止事件循環到達 輪詢 階段。

一個題目

// test.js
process.nextTick(function() {
  console.log('next tick');
});

setTimeout(function() {
  console.log('settimeout');
});

(async function() {
  console.log('async promise');
})();

setImmediate(function() {
  console.log('setimmediate');
});

$ node test.js
async promise
next tick
settimeout
setimmediate
  • 沒有await,async那句其實是同步執行的,故而第一句輸出。
  • next tick 在任何事件循環階段繼續之前得到解決,故而第二句
  • setTimeout 在主線程中與 setImmediate 的執行順序是非確定性的
// test.js
setTimeout(function () {
  process.nextTick(function() {
    console.log('next tick');
  });
  
  setTimeout(function() {
    console.log('settimeout');
  });
  
  (async function() {
    console.log('async promise');
  })();
  
  setImmediate(function() {
    console.log('setimmediate');
  });
})

$ node test.js
async promise
next tick
setimmediate
settimeout

setimmediate 與 settimeout 放入一個 I/O 循環內調用,則 setImmediate 總是被優先調用

node >= 11 ?

setTimeout(()=>{
    console.log('timer1')
    setImmediate(function () { console.log('immd 1'); })
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    setImmediate(function () { console.log('immd 2'); })
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

在 node 11 及以上版本打印得

timer1
promise1
timer2
promise2
immd 1
immd 2

在 node 版本為 8.11.2 打印

timer1
timer2
promise1
promise2
immd 1
immd 2

這是因為 < 11 得版本中

若第一個定時器任務出隊並執行完,發現隊首的任務仍然是一個定時器,那么就將微任務暫時保存,直接去執行新的定時器任務,當新的定時器任務執行完后,再一一執行中途產生的微任務。

nodejs 和 瀏覽器關於eventLoop的主要區別

兩者最主要的區別在於瀏覽器中的微任務是在每個相應的宏任務中執行的,而nodejs中的微任務是在不同階段之間執行的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM