跳到主要内容

Node.js Event Loop

问题

Node.js 的事件循环是如何工作的?它与浏览器的事件循环有什么区别?

答案

Node.js 的事件循环由 libuv 实现,分为六个阶段,每个阶段处理特定类型的回调。理解事件循环对于编写高性能 Node.js 应用至关重要。


事件循环六个阶段

阶段说明回调来源
timers执行 setTimeout/setInterval 回调定时器
pending callbacks执行延迟到下一次循环的 I/O 回调系统操作
idle, prepare仅供内部使用-
poll获取新的 I/O 事件,执行 I/O 回调网络/文件
check执行 setImmediate 回调setImmediate
close callbacks执行关闭回调(如 socket.on('close')关闭事件

详细解析

1. Timers 阶段

// setTimeout 和 setInterval 的回调在此阶段执行
const start = Date.now();

setTimeout(() => {
console.log(`定时器执行,延迟: ${Date.now() - start}ms`);
}, 100);

// 注意:定时器不保证精确延迟
// 实际延迟可能比指定时间更长
定时器精度

定时器回调的执行时间取决于:

  1. poll 阶段是否有其他待处理事件
  2. 操作系统调度
  3. 同阶段其他定时器回调的执行时间

2. Poll 阶段

Poll 是最重要的阶段,处理大部分 I/O 回调:

import { readFile } from 'fs';

// 文件读取回调在 poll 阶段执行
readFile('file.txt', (err, data) => {
console.log('文件读取完成');
});

Poll 阶段的行为:

  1. 如果 poll 队列不为空,遍历执行回调
  2. 如果 poll 队列为空:
    • 如果有 setImmediate,进入 check 阶段
    • 如果有定时器到期,进入 timers 阶段
    • 否则阻塞等待新的 I/O 事件

3. Check 阶段

// setImmediate 回调在 check 阶段执行
setImmediate(() => {
console.log('setImmediate 执行');
});

4. Close Callbacks 阶段

import { createServer } from 'net';

const server = createServer();
const socket = server.listen(3000);

socket.on('close', () => {
// 在 close callbacks 阶段执行
console.log('Socket 已关闭');
});

process.nextTick 与 微任务

process.nextTick 和 Promise 微任务不属于事件循环的任何阶段,它们在每个阶段结束后立即执行。

// 执行顺序演示
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
console.log('同步代码');

// 输出顺序:
// 同步代码
// nextTick
// Promise
// setTimeout 或 setImmediate(取决于系统)
// setImmediate 或 setTimeout

执行优先级

// 完整示例
const { readFile } = require('fs');

console.log('1: 开始');

setTimeout(() => {
console.log('5: setTimeout');
}, 0);

setImmediate(() => {
console.log('6: setImmediate');
});

process.nextTick(() => {
console.log('3: nextTick');
});

Promise.resolve().then(() => {
console.log('4: Promise');
});

readFile(__filename, () => {
console.log('7: I/O callback');

setTimeout(() => console.log('9: setTimeout in I/O'), 0);
setImmediate(() => console.log('8: setImmediate in I/O'));
process.nextTick(() => console.log('nextTick in I/O'));
});

console.log('2: 结束');

// 输出顺序:
// 1: 开始
// 2: 结束
// 3: nextTick
// 4: Promise
// 5: setTimeout(可能与6互换)
// 6: setImmediate(可能与5互换)
// 7: I/O callback
// nextTick in I/O
// 8: setImmediate in I/O
// 9: setTimeout in I/O

setTimeout vs setImmediate

// 情况一:主模块中调用(顺序不确定)
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
// 顺序取决于系统性能和事件循环启动时间

// 情况二:I/O 回调中调用(setImmediate 先执行)
import { readFile } from 'fs';

readFile(__filename, () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
});
// 输出:
// setImmediate
// setTimeout
// 因为 I/O 回调在 poll 阶段执行,poll 后是 check 阶段

Node.js vs 浏览器事件循环

特性Node.js浏览器
实现libuv浏览器引擎
阶段6 个阶段任务队列 + 微任务
微任务时机每个阶段后执行(Node 11+)每个宏任务后执行
setImmediate✅ 有❌ 无
process.nextTick✅ 有❌ 无
requestAnimationFrame❌ 无✅ 有
queueMicrotask✅ 有✅ 有
// 浏览器中的微任务执行时机
// 每个宏任务后执行所有微任务

// Node.js 11+ 也改为每个宏任务后执行微任务
// 与浏览器保持一致
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => console.log('promise2'));
}, 0);

// Node.js 11+ 和浏览器输出:
// timeout1
// promise1
// timeout2
// promise2

// Node.js 10 及之前输出:
// timeout1
// timeout2
// promise1
// promise2

实际应用

避免阻塞事件循环

// ❌ 阻塞事件循环
function processLargeArray(arr: number[]) {
arr.forEach((item) => {
heavyComputation(item);
});
}

// ✅ 分批处理,让出事件循环
async function processLargeArrayAsync(arr: number[]) {
const batchSize = 1000;

for (let i = 0; i < arr.length; i += batchSize) {
const batch = arr.slice(i, i + batchSize);
batch.forEach((item) => heavyComputation(item));

// 让出事件循环
await new Promise((resolve) => setImmediate(resolve));
}
}

控制执行顺序

// 确保回调在当前操作完成后执行
function doSomething(callback: () => void) {
// 使用 process.nextTick 确保异步但立即执行
process.nextTick(callback);
}

// 确保回调在下一次事件循环执行
function doSomethingLater(callback: () => void) {
setImmediate(callback);
}

常见面试问题

Q1: Node.js 事件循环有哪些阶段?

答案

Node.js 事件循环有 6 个阶段:

  1. timers:执行 setTimeout/setInterval 回调
  2. pending callbacks:执行延迟的 I/O 回调
  3. idle, prepare:内部使用
  4. poll:获取 I/O 事件,执行 I/O 回调
  5. check:执行 setImmediate 回调
  6. close callbacks:执行关闭事件回调
// 每个阶段都有一个 FIFO 队列
// 事件循环依次执行每个阶段的回调
// 直到队列为空或达到最大回调数,然后进入下一阶段

Q2: process.nextTick 和 setImmediate 的区别?

答案

特性process.nextTicksetImmediate
执行时机当前阶段结束后立即check 阶段
递归调用可能阻塞 I/O不会阻塞
推荐使用需要在事件前执行一般异步操作
// process.nextTick 递归会阻塞
function recursiveNextTick() {
process.nextTick(recursiveNextTick);
}
// ❌ 会阻塞事件循环

function recursiveImmediate() {
setImmediate(recursiveImmediate);
}
// ✅ 不会阻塞,每次迭代让出控制权

Q3: 为什么 I/O 回调中 setImmediate 总是先于 setTimeout?

答案

因为 I/O 回调在 poll 阶段执行,poll 阶段后是 check 阶段(setImmediate),然后是 close callbacks,最后回到 timers 阶段(setTimeout)。

import { readFile } from 'fs';

readFile(__filename, () => {
// 现在在 poll 阶段
setTimeout(() => console.log('setTimeout'), 0); // 下一轮 timers
setImmediate(() => console.log('setImmediate')); // 本轮 check
});

// 输出:
// setImmediate
// setTimeout

Q4: 如何检测事件循环阻塞?

答案

// 方法一:监控 setImmediate 延迟
let lastTime = Date.now();

setInterval(() => {
const now = Date.now();
const delay = now - lastTime - 100; // 100ms 间隔

if (delay > 10) {
console.warn(`事件循环延迟: ${delay}ms`);
}

lastTime = now;
}, 100);

// 方法二:使用 perf_hooks
import { monitorEventLoopDelay } from 'perf_hooks';

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

setInterval(() => {
console.log(`Event loop delay:
min: ${histogram.min / 1e6}ms
max: ${histogram.max / 1e6}ms
mean: ${histogram.mean / 1e6}ms
`);
histogram.reset();
}, 5000);

// 方法三:blocked-at 库
// npm install blocked-at
import blocked from 'blocked-at';

blocked((time, stack) => {
console.log(`Blocked for ${time}ms, operation started:`, stack);
});

Q5: 如何优化事件循环性能?

答案

// 1. 避免同步 I/O
// ❌
const data = fs.readFileSync('file.txt');

// ✅
const data = await fs.promises.readFile('file.txt');

// 2. 分割 CPU 密集任务
// ❌
function processAll(items: number[]) {
items.forEach(heavyTask);
}

// ✅
async function processAllAsync(items: number[]) {
for (let i = 0; i < items.length; i++) {
heavyTask(items[i]);
if (i % 100 === 0) {
await new Promise(r => setImmediate(r));
}
}
}

// 3. 使用 Worker Threads 处理 CPU 密集任务
import { Worker } from 'worker_threads';

// 4. 调整线程池大小
process.env.UV_THREADPOOL_SIZE = '8';

// 5. 避免递归 nextTick
// ❌
function badRecursion() {
process.nextTick(badRecursion);
}

// ✅ 使用 setImmediate
function goodRecursion() {
setImmediate(goodRecursion);
}

相关链接