Node.js 事件迴圈 (Event Loop) 核心機制詳解

Node.js 最著名的標籤就是「非同步、非阻塞 I/O」,而達成這一切的核心靈魂就是 事件迴圈 (Event Loop)。雖然我們在撰寫程式時感覺它似乎能同時處理很多事情,但實際上,Node.js 依然是在單執行緒上運行,透過不斷循環的機制來調度任務。

事件迴圈的六大主要階段

當 Node.js 啟動時,它會初始化事件迴圈。每一輪循環我們稱之為一個 Tick。Libuv 會按照嚴格的順序執行以下六個主要的階段:

  1. Timers (定時器階段):處理 setTimeout()setInterval() 到期的回呼函數。
  2. Pending Callbacks (待處理階段):執行某些系統操作的回呼,例如 TCP 連線錯誤。
  3. Idle, Prepare (空閒與準備階段):僅供 Node.js 內部系統使用。
  4. Poll (輪詢階段):這是最關鍵的階段。它會檢索新的 I/O 事件。如果隊列為空,它會在這裡等待(阻塞)一段時間,直到有事件進來或 Timer 到期。
  5. Check (檢查階段):專門執行 setImmediate() 的回呼函數。
  6. Close Callbacks (關閉回呼階段):處理如 socket.on('close', ...) 之類資源釋放的事件。

微任務 (Microtasks) 與 nextTick 的插隊機制

在上述階段轉換的空隙中,還有兩個非常特別的存在,我們統稱為「微任務」。它們不屬於事件迴圈的正式階段,但優先權極高:

  • process.nextTick():它的優先權最高。只要當前階段的 JavaScript 操作一結束,Node.js 就會先清空所有的 nextTick 隊列,然後才會進入下一個階段。
  • Promises (微任務隊列):如 Promise.then()。它的優先權僅次於 nextTick

執行優先順序公式: 同步代碼 > process.nextTick() > Promise 微任務 > 事件迴圈階段 (如 Timers, Check)

程式碼實戰:執行順序大挑戰

理解理論後,讓我們透過一段程式碼來驗證執行順序:

console.log('1. 同步代碼 - 開始');

setTimeout(() => {
  console.log('5. setTimeout (Timers 階段)');
}, 0);

setImmediate(() => {
  console.log('6. setImmediate (Check 階段)');
});

process.nextTick(() => {
  console.log('3. process.nextTick (極高優先權微任務)');
});

Promise.resolve().then(() => {
  console.log('4. Promise.then (微任務)');
});

console.log('2. 同步代碼 - 結束');

解析執行邏輯:

  1. 首先執行所有的同步代碼 (1 & 2)。
  2. 同步任務結束,進入微任務檢查點。process.nextTick 優先插隊執行 (3)。
  3. 接著處理 Promise.then 隊列 (4)。
  4. 進入事件迴圈第一階段 Timers,執行 setTimeout (5)。
  5. 經過 Poll 階段後進入 Check 階段,執行 setImmediate (6)。

為什麼開發者需要深入了解事件迴圈?

  1. 防止效能瓶頸 (Starvation):如果你在任何一個階段(如 Poll)執行了耗時過長的同步運算,事件迴圈就會卡死,導致所有定時器(Timers)失效,伺服器停止回應。
  2. 精確的非同步控制:正確選擇 setImmediate (稍後執行) 或 nextTick (立即插隊),能讓你寫出更高效且符合預期的代碼。
警告:請謹慎使用 process.nextTick()。如果您在 nextTick 中遞迴地呼叫自己,將會導致事件迴圈永遠無法進入下一個正式階段,造成應用程式假死。

總結

事件迴圈是 Node.js 效能優勢的來源。記住:不要阻塞 Event Loop。將耗時的運算交給其他微服務或 Worker Threads 處理,讓 Event Loop 始終保持流暢,是開發高品質 Node.js 應用的基本功。