强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 7 章 · 事件循环

第 7 章 · 事件循环

7.1 事件循环概述

事件循环(Event Loop)是 Node.js 的核心机制,它让单线程的 JavaScript 能够处理并发操作。

   ┌───────────────────────────┐
┌─>│         timers            │ ← setTimeout / setInterval
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │     pending callbacks     │ ← 系统级回调(TCP 错误等)
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │       idle, prepare       │ ← 内部使用
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │         poll              │ ← I/O 操作(文件、网络)
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │         check             │ ← setImmediate
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │     close callbacks       │ ← socket.on('close')
│  └──────────┬────────────────┘
│             │
└─────────────┘

7.2 六个阶段详解

Timers 阶段

执行 setTimeoutsetInterval 的回调。

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

setInterval(() => {
  console.log('setInterval');
}, 1000);

Pending Callbacks 阶段

执行被推迟到下一次循环的系统级回调,如 TCP 错误。

Poll 阶段

这是最重要的阶段,负责:

  • 执行 I/O 回调(文件读写、网络请求等)
  • 计算应该阻塞多久等待 I/O
poll 阶段行为:
1. 如果有 I/O 回调 → 执行回调(直到队列为空或达到系统上限)
2. 如果没有回调 →
   a. 如果有 setImmediate → 结束 poll,进入 check
   b. 如果没有 setImmediate → 等待 I/O(有超时时间)

Check 阶段

执行 setImmediate 的回调。

Close Callbacks 阶段

执行关闭事件的回调,如 socket.on('close')

7.3 process.nextTick 与微任务

优先级对比

setTimeout(() => console.log('1. setTimeout'), 0);
setImmediate(() => console.log('2. setImmediate'));

Promise.resolve().then(() => console.log('3. Promise'));
process.nextTick(() => console.log('4. process.nextTick'));
queueMicrotask(() => console.log('5. queueMicrotask'));

console.log('6. 同步代码');

// 输出顺序:
// 6. 同步代码
// 4. process.nextTick    ← 最高优先级
// 5. queueMicrotask
// 3. Promise
// 1. setTimeout 或 2. setImmediate(顺序不确定)

微任务队列

宏任务队列(事件循环各阶段):
  timers → pending → poll → check → close

微任务队列(每个阶段之间清空):
  process.nextTick → Promise.then → queueMicrotask

执行顺序:
1. 执行当前宏任务
2. 清空所有微任务(nextTick 优先于 Promise)
3. 进入下一个宏任务阶段
// 微任务会在每个阶段之间执行
const fs = require('fs');

fs.readFile(__filename, () => {
  // I/O 回调(poll 阶段)
  console.log('1. I/O 回调');
  
  setTimeout(() => console.log('2. setTimeout(timers)'), 0);
  setImmediate(() => console.log('3. setImmediate(check)'));
  
  process.nextTick(() => console.log('4. nextTick'));
  Promise.resolve().then(() => console.log('5. Promise'));
});

// 输出:
// 1. I/O 回调
// 4. nextTick
// 5. Promise
// 3. setImmediate(check 在 timers 之前)
// 2. setTimeout

7.4 process.nextTick 的危险

// ❌ 递归 nextTick 会阻塞事件循环
function dangerous() {
  process.nextTick(dangerous); // 永远不会让出控制权
}
// dangerous(); // 不要这样做!

// ✅ 使用 setImmediate 让出控制权
function cooperative() {
  setImmediate(cooperative); // 每次迭代让出控制权
}

// 对比:递归 nextTick vs setImmediate
process.nextTick(() => console.log('nextTick 1'));
process.nextTick(() => console.log('nextTick 2'));
setImmediate(() => console.log('immediate 1'));
setImmediate(() => console.log('immediate 2'));

// nextTick 1
// nextTick 2
// immediate 1
// immediate 2

7.5 实战:理解事件循环顺序

// test-event-loop.js
const fs = require('fs');

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

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

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

// I/O
fs.readFile(__filename, () => {
  console.log('7. Poll: I/O 完成');
  
  setTimeout(() => console.log('8. Timer: I/O 内 setTimeout'), 0);
  setImmediate(() => console.log('9. Check: I/O 内 setImmediate'));
});

// NextTick
process.nextTick(() => {
  console.log('3. 微任务: process.nextTick');
});

// Microtask
Promise.resolve().then(() => {
  console.log('4. 微任务: Promise.then');
});

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

// 输出:
// 1. 同步: 开始
// 2. 同步: 结束
// 3. 微任务: process.nextTick
// 4. 微任务: Promise.then
// 5. Timer: setTimeout
// 6. Check: setImmediate
// 7. Poll: I/O 完成
// 8. Timer: I/O 内 setTimeout
// 9. Check: I/O 内 setImmediate

7.6 定时器精度

// setTimeout 的最小延迟
const start = Date.now();
setTimeout(() => {
  console.log(`实际延迟: ${Date.now() - start}ms`);
}, 1);
// 实际延迟通常为 1-2ms,但系统负载高时可能更大

// setTimeout(fn, 0) 不等于立即执行
setTimeout(() => console.log('A'), 0);
setImmediate(() => console.log('B'));
// 顺序不确定!取决于进入事件循环的时间

// 纳秒级计时
const { performance, PerformanceObserver } = require('perf_hooks');
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(3)}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

performance.mark('start');
setTimeout(() => {
  performance.mark('end');
  performance.measure('setTimeout', 'start', 'end');
}, 0);

注意事项

⚠️ process.nextTick 不是 setTimeout 的替代品:递归调用 nextTick 会阻止 I/O 和其他回调执行,导致"饥饿"问题。

⚠️ setTimeout(fn, 0) 实际延迟大于 0:Node.js 内部最小延迟为 1ms,系统负载高时可能更大。

⚠️ 在 I/O 循环内 setImmediate 总是先于 setTimeout:这是唯一可以确定两者顺序的场景。

扩展阅读


上一章第 6 章 · 异步编程基础 下一章第 8 章 · 流(Streams) — 可读、可写、Transform、Duplex 和管道。