深入理解 Node.js Event Loop

Node.js 是單執行緒的,但它能同時處理大量 I/O 請求。這背後的核心機制就是 Event Loop,由底層的 C 函式庫 libuv 實作。

為什麼要理解 Event Loop?

看過這段程式碼嗎?

setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');

輸出順序是:

sync
nextTick
Promise
setTimeout

如果不清楚 Event Loop 的運作,這個結果會讓人困惑。看完這篇文章你會完全理解原因。


Event Loop 的六個階段

libuv 的 Event Loop 每跑一圈(tick),會依序經過六個階段:

   ┌───────────────────────────┐
┌─>│         timers            │  ← setTimeout / setInterval callback
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  ← 上一輪延遲的 I/O error callback
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  ← libuv 內部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  ← 取得新的 I/O 事件,執行 I/O callback
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  ← setImmediate callback
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──│      close callbacks      │  ← socket.on('close', ...) 等
   └───────────────────────────┘

1. timers

執行 setTimeoutsetInterval 的 callback。但「delay 到期」只是進入候補資格,實際執行時間取決於 poll 階段花了多久。

// 這個 callback 不保證剛好 100ms 後執行
// 只保證「至少」100ms 後才會執行
setTimeout(() => console.log('timer'), 100);

2. pending callbacks

執行上一輪 Event Loop 延遲的 I/O error callback,例如 TCP 連線錯誤的通知。通常不需要直接處理這個階段。

3. idle, prepare

libuv 內部使用,Node.js 層不暴露。

4. poll

這是最重要的階段,負責兩件事:

  1. 計算需要 block 多久等待 I/O
  2. 執行 I/O 相關的 callback(檔案讀取、網路請求等)

如果 timers 有到期的 callback,poll 會結束並跳回 timers 階段。如果沒有,則會在這裡等待新的 I/O 事件。

5. check

執行 setImmediate 的 callback。setImmediate 保證在當前 poll 階段結束後、下一輪 timers 之前執行。

const fs = require('fs');

fs.readFile('file.txt', () => {
  setTimeout(() => console.log('setTimeout'), 0);
  setImmediate(() => console.log('setImmediate'));
});

// 在 I/O callback 內,setImmediate 永遠先於 setTimeout
// 輸出:setImmediate → setTimeout

6. close callbacks

執行 close 事件的 callback,例如 socket.destroy() 後觸發的 socket.on('close', ...)


Microtask Queue:在每個階段之間執行

除了六個階段,還有兩個特殊的 queue 會在每個階段切換之前清空:

  1. process.nextTick queue
  2. Promise microtask queuePromise.thenasync/await

執行優先序:process.nextTick > Promise microtask > 下一個 Event Loop 階段

setTimeout(() => console.log('1. setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('2. Promise'));

process.nextTick(() => console.log('3. nextTick'));

console.log('4. sync');

執行順序:

4. sync        ← 同步程式碼先跑完
3. nextTick    ← nextTick queue 清空
2. Promise     ← microtask queue 清空
1. setTimeout  ← 進入下一個 Event Loop,timers 階段

process.nextTick 的陷阱

process.nextTick 不屬於 Event Loop 的任何一個階段,它在當前操作完成後立即執行,優先度最高。

這也意味著:濫用 process.nextTick 可能讓 I/O 永遠無法執行。

function infiniteNextTick() {
  process.nextTick(infiniteNextTick);
}

infiniteNextTick();
fs.readFile('file.txt', () => {
  // 這行永遠不會執行
  console.log('file read');
});

Node.js 官方建議:能用 setImmediate 就用 setImmediate,除非你明確需要在 I/O 之前執行。


async/await 的本質

async/await 是 Promise 的語法糖,await 之後的程式碼等同於 .then() callback,進入 microtask queue。

async function main() {
  console.log('1. async start');
  await Promise.resolve();
  console.log('3. after await'); // microtask
}

main();
console.log('2. sync after main()');

// 輸出:1 → 2 → 3

總結

機制所屬佇列執行時機
同步程式碼Call Stack立即
process.nextTicknextTick Queue當前操作結束後,最優先
Promise.then / awaitMicrotask QueuenextTick 清空後
setImmediatecheck 階段poll 階段結束後
setTimeout(fn, 0)timers 階段下一輪 Event Loop
I/O callbackpoll 階段依事件觸發

理解了這張表,Node.js 裡所有非同步行為的執行順序都能推導出來。