Node.js 事件循环 vs 浏览器事件循环
浏览器和 Node.js 都有事件循环,但实现上有显著差异。
浏览器的事件循环围绕"渲染"展开,每次循环迭代会尝试渲染页面(requestAnimationFrame)。
Node.js 的事件循环围绕"I/O 操作"展开,更细分,有明确的六个阶段。
六个阶段详解
timers 阶段
执行 setTimeout() 和 setInterval() 设置的回调。
setTimeout(() => console.log('timer callback'), 100);
pending callbacks 阶段
执行上一轮延迟到本轮的 I/O 回调。
idle, prepare 阶段
仅供内部使用。
poll 阶段
获取新的 I/O 事件;执行与 I/O 相关的回调。
- 如果 poll 队列非空,依次执行回调
- 如果 poll 队列为空,如果有
setImmediate回调,跳到 check 阶段 - 如果既没有
setImmediate也没有回调,等待新的 I/O 事件
check 阶段
setImmediate() 设置的回调在此执行。
setImmediate(() => console.log('immediate'));
close callbacks 阶段
执行关闭回调,如 socket 关闭事件。
setTimeout vs setImmediate
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
在 REPL 环境和普通脚本中,输出顺序不确定——取决于系统性能。
但在 I/O 操作的回调里,setImmediate 总是先执行:
const fs = require('fs');
fs.readFile('/etc/hostname', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
});
输出顺序:nextTick, immediate, timeout
原因:I/O 回调里 poll 阶段完成后,setImmediate 进入 check 阶段立即执行,而 setTimeout 需要等 timers 阶段。
process.nextTick
process.nextTick 是 Node.js 独有的 API,它的回调优先级高于其他微任务。
Promise.resolve()
.then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
输出:nextTick, promise
process.nextTick 的回调存在一个单独的队列,在每个阶段结束时(但在微任务队列之前)执行。
Node.js 中的微任务
Node.js 的微任务队列有两个优先级:
- nextTick 队列:
process.nextTick()的回调,最高优先级 - 微任务队列:
Promise.then()、.catch()、.finally()的回调
process.nextTick(() => console.log('1'));
Promise.resolve().then(() => console.log('2'));
queueMicrotask(() => console.log('3'));
process.nextTick(() => console.log('4'));
// 输出: 1, 4, 2, 3
常见的 Node.js 异步模式
错误处理的约定
const fs = require('fs').promises;
async function readConfig() {
try {
const data = await fs.readFile('config.json', 'utf-8');
return JSON.parse(data);
} catch (err) {
console.error('Failed to read config:', err);
throw err;
}
}
并行 I/O 操作
const { promises: fs } = require('fs');
async function readAllFiles(files) {
// 并行读取所有文件
const filePromises = files.map(file => fs.readFile(file, 'utf-8'));
return Promise.all(filePromises);
}
这一章想说的
Node.js 的事件循环分为六个阶段:timers → pending callbacks → idle/prepare → poll → check → close callbacks。
关键知识点:
setTimeout在 timers 阶段执行,setImmediate在 check 阶段执行- 在 I/O 回调中,
setImmediate几乎总是先于setTimeout执行 process.nextTick优先级高于普通微任务