Node.js 事件迴圈 (Event Loop) 核心機制詳解
Node.js 最著名的標籤就是「非同步、非阻塞 I/O」,而達成這一切的核心靈魂就是 事件迴圈 (Event Loop)。雖然我們在撰寫程式時感覺它似乎能同時處理很多事情,但實際上,Node.js 依然是在單執行緒上運行,透過不斷循環的機制來調度任務。
事件迴圈的六大主要階段
當 Node.js 啟動時,它會初始化事件迴圈。每一輪循環我們稱之為一個 Tick。Libuv 會按照嚴格的順序執行以下六個主要的階段:
- Timers (定時器階段):處理
setTimeout()和setInterval()到期的回呼函數。 - Pending Callbacks (待處理階段):執行某些系統操作的回呼,例如 TCP 連線錯誤。
- Idle, Prepare (空閒與準備階段):僅供 Node.js 內部系統使用。
- Poll (輪詢階段):這是最關鍵的階段。它會檢索新的 I/O 事件。如果隊列為空,它會在這裡等待(阻塞)一段時間,直到有事件進來或 Timer 到期。
- Check (檢查階段):專門執行
setImmediate()的回呼函數。 - 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 & 2)。
- 同步任務結束,進入微任務檢查點。
process.nextTick優先插隊執行 (3)。 - 接著處理
Promise.then隊列 (4)。 - 進入事件迴圈第一階段
Timers,執行setTimeout(5)。 - 經過 Poll 階段後進入
Check階段,執行setImmediate(6)。
為什麼開發者需要深入了解事件迴圈?
- 防止效能瓶頸 (Starvation):如果你在任何一個階段(如 Poll)執行了耗時過長的同步運算,事件迴圈就會卡死,導致所有定時器(Timers)失效,伺服器停止回應。
- 精確的非同步控制:正確選擇
setImmediate(稍後執行) 或nextTick(立即插隊),能讓你寫出更高效且符合預期的代碼。
警告:請謹慎使用
process.nextTick()。如果您在 nextTick 中遞迴地呼叫自己,將會導致事件迴圈永遠無法進入下一個正式階段,造成應用程式假死。總結
事件迴圈是 Node.js 效能優勢的來源。記住:不要阻塞 Event Loop。將耗時的運算交給其他微服務或 Worker Threads 處理,讓 Event Loop 始終保持流暢,是開發高品質 Node.js 應用的基本功。