Node.js 事件迴圈:初學者完整解析

Backend2026-09-14TryQuickToolBox

你可能聽過 Node.js 是單執行緒,並使用事件迴圈來處理數千個並行連線。但這到底是什麼意思?如果你剛接觸 Node.js,事件迴圈可能像個神祕的黑盒子。本文將用淺顯易懂的語言拆解它,讓你寫出更好、更快的 Node.js 程式碼。

為什麼需要事件迴圈

傳統的網頁伺服器(例如搭配 PHP 的 Apache)會為每個請求建立新的執行緒或行程。這雖然可行,但當你有很多並行連線時,擴展性並不好——每個執行緒都會消耗記憶體和 CPU。Node.js 採用了不同的做法:它在單一執行緒中執行你的 JavaScript,並使用事件迴圈來非同步處理 I/O 操作。這表示你的程式碼不會等待資料庫查詢或檔案讀取完成;它會繼續執行其他任務,並在結果準備好時回來處理。

核心元件

在深入事件迴圈之前,讓我們先釐清幾個關鍵角色:

事件迴圈如何運作:一個簡單範例

看看這段程式碼:

console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

console.log('End');

輸出是什麼?如果你猜 Start、End、Timeout,那就對了。即使逾時設定為 0 毫秒,回呼也不會立即執行。以下是實際發生的過程:

  1. console.log('Start') 被推入呼叫堆疊,執行後彈出。
  2. setTimeout 被呼叫。Node.js 註冊計時器並設定在 0 毫秒後到期。回呼被儲存,函式返回。
  3. console.log('End') 被推入、執行、彈出。
  4. 現在呼叫堆疊是空的。事件迴圈檢查計時器階段,發現計時器已到期。它將回呼移到呼叫堆疊,記錄 Timeout。

這展示了非阻塞的特性:計時器不會阻塞其餘程式碼。

事件迴圈的階段

事件迴圈在幾個階段中處理回呼,每個階段都有特定目的。了解這些階段有助於你預測執行順序。

階段 說明
計時器(Timers) 執行由 setTimeout() 和 setInterval() 排定的回呼。
待處理回呼(Pending Callbacks) 執行延遲到下一個迴圈迭代的 I/O 回呼。
閒置、準備(Idle, Prepare) Node.js 內部使用。
輪詢(Poll) 擷取新的 I/O 事件;執行 I/O 相關回呼(幾乎所有,除了計時器、setImmediate 和關閉回呼)。
檢查(Check) 執行由 setImmediate() 排定的回呼。
關閉回呼(Close Callbacks) 執行關閉回呼,例如 socket.on('close', ...)。

在每個階段之間,Node.js 會檢查微任務(microtasks):process.nextTick() 回呼和 Promise。這些具有較高優先級,會在目前操作完成後立即執行,然後才進入下一個階段。

微任務:nextTick 與 Promise

微任務不屬於事件迴圈的階段;它們在每個階段之後和每個回呼之後被處理。這使它們在計時器和 I/O 回呼之前執行。例如:

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

輸出:Sync、nextTick、Promise、Timeout。同步程式碼先執行,然後是微任務(nextTick 在 Promise 之前),最後是計時器。

警告: 遞迴呼叫 process.nextTick() 會耗盡事件迴圈,阻止 I/O 發生。遞迴操作請使用 setImmediate()。

setImmediate 與 setTimeout 的比較

setImmediate() 的設計是在目前的輪詢階段完成後立即執行回呼。相對地,0 毫秒的 setTimeout() 會等待下一個計時器階段。它們之間的順序可能因情境而異,但在 I/O 回呼內部,setImmediate() 總是先執行。

為什麼這對你的程式碼很重要

了解事件迴圈有助於你避免常見的陷阱:

實用範例:非阻塞檔案讀取

以下是事件迴圈如何實現非阻塞 I/O:

const fs = require('fs');

console.log('Before read');

fs.readFile('large-file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('File read complete');
});

console.log('After read');

輸出:Before read、After read,然後是 File read complete。檔案讀取在背景進行,完成後回呼會被排入佇列。同時,其他程式碼可以繼續執行。

常見問題

Node.js 真的是單執行緒嗎?

Node.js 在單一執行緒中執行你的 JavaScript,但它使用 libuv 執行緒池中的多個執行緒來處理檔案 I/O、DNS 和其他操作。所以底層並非完全單執行緒。

呼叫堆疊和事件迴圈有什麼區別?

呼叫堆疊執行同步程式碼。事件迴圈管理非同步回呼,在堆疊為空時將它們從佇列移到呼叫堆疊。

我可以建立多個事件迴圈嗎?

不行,每個 Node.js 行程只有一個事件迴圈。但是,你可以使用工作執行緒來執行獨立的 JavaScript 執行緒,每個都有自己的事件迴圈。

如果你正在處理伺服器日誌以偵錯效能問題,試試我們的 Nginx 日誌分析器,快速解析並視覺化日誌資料。