Node.js事件循环机制详解
什么是事件循环?
事件循环是Node.js最核心的机制之一,理解它对于写出高性能的Node.js代码至关重要。简单来说,事件循环让Node.js能够处理大量并发请求,而不需要为每个请求创建新线程。
为什么需要事件循环?
在传统的服务器端语言中(如Java、PHP),每个请求通常会创建一个新线程。假设你的服务器同时有1000个请求,就需要1000个线程。但线程是昂贵的——每个线程占用约1MB内存,1000个线程就是1GB!而且线程切换也有开销。
Node.js采用完全不同的方式:单线程 + 事件循环。一个线程处理所有请求,通过事件循环来协调异步操作。
事件循环的6个阶段
事件循环有6个阶段,每个阶段都有一个先进先出的回调队列:
┌───────────────────────┐
┌──> timers │<── setTimeout/setInterval
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ pending callbacks │<── 某些系统操作
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ idle, prepare │<── 内部使用
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ poll │<── I/O回调、新事件
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ check │<── setImmediate
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
└──┤ close callbacks │<── socket.close()
└───────────────────────┘
1. Timers阶段
执行setTimeout()和setInterval()的回调。注意,定时器不保证精确时间,只是在指定时间后尽快执行。
setTimeout(() => {
console.log('定时器执行了');
}, 100);
// 即使设置为100ms,实际执行时间可能更长
2. Pending Callbacks阶段
执行某些系统操作的回调,比如TCP错误。通常很少接触。
3. Poll阶段(最重要)
这是事件循环停留时间最长的阶段:
- 执行I/O回调
- 如果队列为空,检查是否有定时器到期
- 如果有定时器到期,回到timers阶段
- 如果没有定时器,等待新事件
4. Check阶段
执行setImmediate()的回调:
const fs = require('fs');
fs.readFile('./file.txt', () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
});
// 输出顺序:setImmediate -> setTimeout(在I/O回调中)
5. Close Callbacks阶段
执行关闭回调,如socket.on('close', ...)。
实战示例:理解执行顺序
console.log('1. 脚本开始');
setTimeout(() => console.log('2. setTimeout'), 0);
setImmediate(() => console.log('3. setImmediate'));
process.nextTick(() => console.log('4. nextTick'));
Promise.resolve().then(() => console.log('5. Promise'));
console.log('6. 脚本结束');
// 输出:
// 1. 脚本开始
// 6. 脚本结束
// 4. nextTick
// 5. Promise
// 2. setTimeout 或 3. setImmediate(顺序不确定)
process.nextTick() vs setImmediate()
| 特性 | process.nextTick() | setImmediate() |
|---|---|---|
| 执行时机 | 每个阶段之间 | check阶段 |
| 优先级 | 最高 | 较低 |
| 用途 | 紧急任务 | I/O后的任务 |
注意:nextTick优先级太高,滥用会阻塞事件循环!
实际应用场景
1. 处理大量并发请求
const http = require('http');
const server = http.createServer((req, res) => {
// 不阻塞事件循环
setTimeout(() => {
res.end('处理完成');
}, 100);
});
server.listen(3000);
2. 合理拆分CPU密集任务
// 错误:阻塞事件循环
function heavyCalculation(n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.random();
}
return result;
}
// 正确:分批处理
async function heavyCalculationAsync(n, batchSize = 1000000) {
let result = 0;
let processed = 0;
while (processed < n) {
const end = Math.min(processed + batchSize, n);
for (let i = processed; i < end; i++) {
result += Math.random();
}
processed = end;
// 让出控制权给事件循环
await new Promise(resolve => setImmediate(resolve));
}
return result;
}
调试技巧
使用--trace-warnings查看事件循环延迟:
node --trace-warnings app.js
小结
- 事件循环让Node.js高效处理并发
- 理解6个阶段有助于写出更好的代码
- nextTick优先级最高,谨慎使用
- CPU密集任务要分批处理,避免阻塞事件循环