想象你在一家咖啡店點單,服務員如果只盯着一個顧客,等他喝完咖啡才接下一個,那整個店的效率會很低。但如果服務員能“同時”處理多個顧客的訂單——先記下訂單,然後去準備咖啡,期間再處理其他顧客的需求,最後把咖啡送過去,整體速度就會快很多。Node.js處理請求的邏輯,和這個聰明的服務員類似,而事件循環就是背後的核心機制。
一、單線程與非阻塞:Node.js的“快速密碼”¶
首先得明確:Node.js是單線程的。這意味着同一時間只能執行一段代碼,不能像多線程程序那樣同時跑多個任務。但這並不意味着Node.js“慢”——恰恰相反,它能高效處理大量併發請求,靠的是非阻塞I/O和事件循環的協作。
舉個例子:如果用傳統的服務器(比如Java的Tomcat)處理“讀取文件”這樣的操作,程序會一直“卡住”,直到文件讀取完成才能繼續處理其他請求。而Node.js不會這麼做:當你調用fs.readFile(讀取文件)時,Node.js會把這個任務交給底層的libuv庫(處理異步I/O的工具),自己則立刻去處理下一個請求。等文件讀取完成後,libuv會把“讀取完成”的消息(即回調函數)放進一個任務隊列,事件循環會“輪詢”這個隊列,把任務取出來執行。
二、事件循環:如何“同時”處理多個任務?¶
事件循環就像一個“調度員”,負責按順序處理任務隊列中的異步操作。它的工作流程可以拆解爲“階段化處理”:每個階段對應一類異步任務,按固定順序執行,直到所有任務處理完畢。
核心階段(簡化版,適合初學者):¶
-
Timers(定時器):處理
setTimeout和setInterval的回調。比如你設置了setTimeout(fn, 1000),fn會在1秒後被放進這個階段的隊列。 -
Pending Callbacks(延遲迴調):處理系統級的回調(比如TCP錯誤、DNS查詢等),日常開發中較少接觸。
-
Idle/Prepare(內部準備):Node.js內部使用,用於優化性能,不用太關注。
-
Poll(輪詢):最重要的階段!它負責等待I/O事件(如文件讀取完成、網絡請求返回),並執行對應的回調。如果沒有I/O事件,會“阻塞”在這裏等待新事件。
-
Check(立即回調):處理
setImmediate的回調(setImmediate會在當前poll階段完成後立即執行)。 -
Close Callbacks(關閉回調):處理關閉事件的回調(如
socket.on('close'))。
三、異步模型:調用棧、任務隊列與事件循環¶
理解事件循環的關鍵,是理解JavaScript的異步執行模型:
-
調用棧:同步代碼執行的“舞臺”。比如
console.log('A')會先進入調用棧執行,執行完後彈出。 -
異步任務:遇到異步操作(如
setTimeout、fs.readFile)時,會被交給libuv處理,不會阻塞調用棧。 -
任務隊列:異步操作完成後,回調函數會被放入隊列(如
setTimeout的回調在Timers隊列,fs.readFile的回調在Poll隊列)。 -
事件循環:不斷檢查任務隊列,按階段順序取出任務,放入調用棧執行,直到所有隊列清空。
四、爲什麼這樣設計讓Node.js這麼快?¶
-
非阻塞I/O:避免等待I/O操作(如文件讀取、網絡請求),讓CPU在等待期間可以處理其他請求。比如,用戶A的請求在等待數據庫返回時,Node.js可以處理用戶B的請求,無需浪費時間“乾等”。
-
高效的回調調度:事件循環按階段順序處理任務,每個階段有固定的隊列,避免了多線程切換的開銷(多線程需要頻繁保存/恢復上下文,反而更慢)。
-
單線程+異步:雖然是單線程,但通過異步回調讓“併發”成爲可能。比如,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處理大量請求時,不妨想想那個“聰明的服務員”和它背後的事件循環機制吧!