想象你在一家咖啡店點單,服務員如果只盯着一個顧客,等他喝完咖啡才接下一個,那整個店的效率會很低。但如果服務員能“同時”處理多個顧客的訂單——先記下訂單,然後去準備咖啡,期間再處理其他顧客的需求,最後把咖啡送過去,整體速度就會快很多。Node.js處理請求的邏輯,和這個聰明的服務員類似,而事件循環就是背後的核心機制。

一、單線程與非阻塞:Node.js的“快速密碼”

首先得明確:Node.js是單線程的。這意味着同一時間只能執行一段代碼,不能像多線程程序那樣同時跑多個任務。但這並不意味着Node.js“慢”——恰恰相反,它能高效處理大量併發請求,靠的是非阻塞I/O和事件循環的協作。

舉個例子:如果用傳統的服務器(比如Java的Tomcat)處理“讀取文件”這樣的操作,程序會一直“卡住”,直到文件讀取完成才能繼續處理其他請求。而Node.js不會這麼做:當你調用fs.readFile(讀取文件)時,Node.js會把這個任務交給底層的libuv庫(處理異步I/O的工具),自己則立刻去處理下一個請求。等文件讀取完成後,libuv會把“讀取完成”的消息(即回調函數)放進一個任務隊列,事件循環會“輪詢”這個隊列,把任務取出來執行。

二、事件循環:如何“同時”處理多個任務?

事件循環就像一個“調度員”,負責按順序處理任務隊列中的異步操作。它的工作流程可以拆解爲“階段化處理”:每個階段對應一類異步任務,按固定順序執行,直到所有任務處理完畢。

核心階段(簡化版,適合初學者):

  1. Timers(定時器):處理setTimeoutsetInterval的回調。比如你設置了setTimeout(fn, 1000)fn會在1秒後被放進這個階段的隊列。

  2. Pending Callbacks(延遲迴調):處理系統級的回調(比如TCP錯誤、DNS查詢等),日常開發中較少接觸。

  3. Idle/Prepare(內部準備):Node.js內部使用,用於優化性能,不用太關注。

  4. Poll(輪詢):最重要的階段!它負責等待I/O事件(如文件讀取完成、網絡請求返回),並執行對應的回調。如果沒有I/O事件,會“阻塞”在這裏等待新事件。

  5. Check(立即回調):處理setImmediate的回調(setImmediate會在當前poll階段完成後立即執行)。

  6. Close Callbacks(關閉回調):處理關閉事件的回調(如socket.on('close'))。

三、異步模型:調用棧、任務隊列與事件循環

理解事件循環的關鍵,是理解JavaScript的異步執行模型

  • 調用棧:同步代碼執行的“舞臺”。比如console.log('A')會先進入調用棧執行,執行完後彈出。

  • 異步任務:遇到異步操作(如setTimeoutfs.readFile)時,會被交給libuv處理,不會阻塞調用棧。

  • 任務隊列:異步操作完成後,回調函數會被放入隊列(如setTimeout的回調在Timers隊列,fs.readFile的回調在Poll隊列)。

  • 事件循環:不斷檢查任務隊列,按階段順序取出任務,放入調用棧執行,直到所有隊列清空。

四、爲什麼這樣設計讓Node.js這麼快?

  1. 非阻塞I/O:避免等待I/O操作(如文件讀取、網絡請求),讓CPU在等待期間可以處理其他請求。比如,用戶A的請求在等待數據庫返回時,Node.js可以處理用戶B的請求,無需浪費時間“乾等”。

  2. 高效的回調調度:事件循環按階段順序處理任務,每個階段有固定的隊列,避免了多線程切換的開銷(多線程需要頻繁保存/恢復上下文,反而更慢)。

  3. 單線程+異步:雖然是單線程,但通過異步回調讓“併發”成爲可能。比如,1000個用戶同時請求,事件循環會逐個處理每個用戶的異步任務,只要不被阻塞,就能高效響應。

五、實戰小例子:看事件循環如何工作

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

// 1. 定時器回調(Timers階段)
setTimeout(() => {
  console.log('定時器回調(setTimeout)');
}, 0);

// 2. 立即回調(Check階段)
setImmediate(() => {
  console.log('立即回調(setImmediate)');
});

// 3. 文件讀取回調(Poll階段)
const fs = require('fs');
fs.readFile('test.txt', (err, data) => {
  console.log('文件讀取回調');
});

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

執行順序
1. 先執行所有同步代碼:輸出 同步代碼開始同步代碼結束
2. 事件循環進入Timers階段,處理setTimeout回調(即使延遲是0,也會等待最小延遲)→ 輸出 定時器回調(setTimeout)
3. 進入Poll階段,此時readFile的回調可能已準備好(假設文件已存在)→ 輸出 文件讀取回調
4. 最後進入Check階段,處理setImmediate → 輸出 立即回調(setImmediate)

(注:實際順序可能因環境略有差異,但核心是按階段順序處理。)

總結

事件循環是Node.js的“心臟”,它通過非阻塞I/O階段化任務調度,讓單線程的JavaScript能高效處理高併發請求。記住:異步代碼的執行順序由事件循環決定,理解它的工作流程,就能寫出更高效的Node.js代碼。下次你用Node.js處理大量請求時,不妨想想那個“聰明的服務員”和它背後的事件循環機制吧!

小夜