一、事件循环机制
Node.js 的事件循环分为六个阶段,每个阶段处理不同类型的任务,按顺序执行,且每个阶段结束后会清空微任务队列:
// 简化版事件循环流程
1. Timers:执行 setTimeout、setInterval 的回调
2. Pending callbacks:处理系统操作(如TCP错误)的回调
3. Idle/Prepare:Node内部使用
4. Poll:执行I/O回调,检索新事件(可能阻塞)
5. Check:执行 setImmediate 回调
6. Close callbacks:处理关闭事件的回调(如socket.on('close'))
执行特点:
- 每个阶段执行完毕后,会先处理
process.nextTick
队列,再处理Promise
微任务队列 - 如果微任务队列被塞满,会导致事件循环阻塞(比如递归调用
process.nextTick
)
二、微任务与宏任务的区别
类型 | 常见API | 执行时机 |
---|---|---|
宏任务 | setTimeout, setInterval | 事件循环的对应阶段(如Timers阶段) |
setImmediate, I/O 操作 | 事件循环的对应阶段(如Check阶段) | |
微任务 | process.nextTick | 每个阶段结束后立即执行(优先级最高) |
Promise.then/catch/finally | 每个阶段结束后,nextTick队列清空后执行 |
执行顺序口诀:
同步代码 > nextTick > Promise > 宏任务
三、代码示例与执行顺序分析
示例1:基础顺序
console.log('同步1');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('同步2');
// 输出顺序:
// 同步1 → 同步2 → nextTick → promise → timeout/immediate 顺序可能互换
// 注意:setTimeout 和 setImmediate 在顶层代码中的执行顺序不确定!
示例2:I/O回调中的确定顺序
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate')); // 必然先输出
process.nextTick(() => console.log('nextTick'));
});
// 输出顺序:nextTick → immediate → timeout
// 原因:I/O回调在Poll阶段,setImmediate属于Check阶段,微任务在阶段切换前处理
示例3:微任务递归导致的饥饿问题
function starve() {
Promise.resolve().then(() => {
console.log('微任务执行');
starve(); // 递归调用导致事件循环无法继续处理宏任务
});
}
starve();
setTimeout(() => console.log('这个永远不会执行'), 0);
// 输出:无限打印"微任务执行",setTimeout回调被阻塞
四、日常开发建议
避免微任务递归
使用while
或递归调用process.nextTick/Promise
会导致事件循环阻塞。耗时任务拆分
将CPU密集型任务拆分成多个宏任务(如用setImmediate
分片):function heavyTask() { let count = 0; function chunk() { while (count < 1e6 && /* 分片条件 */) { // 处理一部分任务 count++; } if (count < 1e6) { setImmediate(chunk); // 让出事件循环 } } chunk(); }
优先使用 setImmediate
在I/O操作后需要立即执行代码时,setImmediate
比setTimeout(fn, 0)
更高效:fs.readFile('file.txt', () => { setImmediate(() => console.log('立即执行')); // 推荐 });
错误处理必须完善
未捕获的异常会导致微任务队列中断:Promise.resolve().then(() => { throw new Error('崩溃'); }).catch(console.error); // 必须捕获!
五、注意事项
浏览器与Node差异
浏览器每执行一个宏任务就会清空微任务队列,而Node.js是在每个阶段结束后清空。nextTick 与 Promise 顺序
process.nextTick
优先级高于Promise
:process.nextTick(() => console.log('A')); Promise.resolve().then(() => console.log('B')); // 输出顺序:A → B
定时器精度问题
setTimeout(fn, 0)
实际延迟至少1ms,而setImmediate
更“立即”。
六、总结
理解事件循环的阶段性特征和任务队列优先级,能帮助开发者避免性能瓶颈和逻辑错误。在异步编程时,应根据任务类型合理选择微任务或宏任务,并始终注意避免阻塞事件循环。