Node.js: 异步编程:事件循环

最后更新:2026-08-26

1. 故事:被 200ms 拦住的服务

Bob 的 API 服务平时响应只要 5ms,但高峰期飙到 2000ms。用户抱怨不断,他一开始以为是数据库慢,加了索引却没改善。后来用 console.time 逐段排查,发现一个同步的 JSON 大数组排序耗时 200ms——这 200ms 事件循环被卡死,所有排队请求都在等它完成。理解事件循环后,Bob 用 setImmediate 把排序拆成小片轮流执行,高峰期响应降到 50ms。

你将学到



2. 事件循环的 6 个阶段

Node.js 事件循环由 libuv 驱动,依次经过 6 个阶段,每轮循环从 timers 开始,到 close callbacks 结束,然后回到 timers 开始下一轮。

100%
flowchart TD
    A["timers<br/>setTimeout / setInterval"] --> B["pending callbacks<br/>系统级回调"]
    B --> C["idle, prepare<br/>内部使用"]
    C --> D["poll<br/>I/O 回调 / 新 I/O 检索"]
    D --> E["check<br/>setImmediate"]
    E --> F["close callbacks<br/>socket.on('close')"]
    F --> A
阶段 回调类型 说明
timers setTimeout / setInterval 执行到期的定时器回调,受最小 1ms 延迟限制
pending callbacks 系统级回调 推迟到下一轮循环执行的 I/O 回调(如 TCP 错误)
idle, prepare 内部使用 libuv 内部使用,开发者一般不接触
poll I/O 回调 检索新 I/O 事件,执行 I/O 相关回调;若无定时器,会在此阶段阻塞等待
check setImmediate setImmediate 回调在此阶段执行
close callbacks close 事件 如 socket.on('close') 等关闭回调


3. 微任务与宏任务

(1) (3.1) 微任务队列(Microtask)

微任务在当前阶段结束后、下一阶段开始前全部执行。微任务队列有两个优先级:

微任务 优先级 说明
process.nextTick 最高 每个阶段结束后优先清空 nextTick 队列
Promise.then / catch / finally 次高 nextTick 队列清空后执行

▶ 示例:nextTick 优先于 Promise.then

JAVASCRIPT
process.nextTick(() => {
  console.log('nextTick 1');
});

Promise.resolve().then(() => {
  console.log('promise.then 1');
});

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

Promise.resolve().then(() => {
  console.log('promise.then 2');
});
▶ 试一试
TEXT 📖 仅展示
nextTick 1
nextTick 2
promise.then 1
promise.then 2

(2) (3.2) 宏任务队列(Macrotask)

宏任务由事件循环各阶段调度,每阶段执行对应的宏任务。

宏任务 所属阶段 说明
setTimeout / setInterval timers 最小延迟 1ms,到期后进入 timers 队列
setImmediate check 下轮循环 check 阶段执行
I/O 回调 poll 文件读写、网络等 I/O 完成回调
close callbacks close callbacks 关闭事件回调


4. setTimeout vs setImmediate vs process.nextTick

▶ 示例:外层上下文执行顺序不确定

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

setImmediate(() => {
  console.log('setImmediate');
});
▶ 试一试

在非 I/O 上下文中,setTimeout 和 setImmediate 的执行顺序不确定——取决于系统进程调度,可能先输出任何一个。

▶ 示例:I/O 上下文中 setImmediate 必定先执行

JAVASCRIPT
const fs = require('fs');

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

  setImmediate(() => {
    console.log('setImmediate');
  });
});
▶ 试一试
TEXT 📖 仅展示
setImmediate
setTimeout

I/O 回调在 poll 阶段执行,结束后进入 check 阶段(setImmediate),然后才到下一轮 timers 阶段(setTimeout)。

▶ 示例:process.nextTick 始终最先

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

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

process.nextTick(() => {
  console.log('nextTick');
});
▶ 试一试
TEXT 📖 仅展示
nextTick
setTimeout   (或 setImmediate,取决于调度)
setImmediate
特性 setTimeout(fn, 0) setImmediate process.nextTick
所属队列 宏任务(timers) 宏任务(check) 微任务(最高优先)
执行阶段 timers check 当前阶段结束后立即
最小延迟 1ms
I/O 上下文顺序 后于 setImmediate 先于 setTimeout 最先
外层上下文顺序 不确定 不确定 最先


5. 微任务 vs 宏任务对比

维度 微任务 宏任务
代表 process.nextTick、Promise.then setTimeout、setImmediate、I/O 回调
执行时机 当前阶段结束后、下一阶段前 事件循环对应阶段中
执行策略 一次性全部清空 每阶段执行一个后检查微任务
嵌套产生 执行中产生的微任务也在本轮清空 推到后续阶段执行
优先级 高于所有宏任务 低于微任务

▶ 示例:微任务穿插在宏任务之间

JAVASCRIPT
setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => {
    console.log('promise between 1 and 2');
  });
}, 0);

setTimeout(() => {
  console.log('timeout 2');
}, 0);
▶ 试一试
TEXT 📖 仅展示
timeout 1
promise between 1 and 2
timeout 2

每个宏任务执行完后,都会清空微任务队列,再执行下一个宏任务。



6. 阻塞事件循环的危害与避免

▶ 示例:阻塞事件循环

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  const start = Date.now();
  while (Date.now() - start < 200) {}
  res.end('done');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});
▶ 试一试

每个请求都会卡住事件循环 200ms,期间所有其他请求都无法处理。

▶ 示例:用 setImmediate 拆分长任务

JAVASCRIPT
function chunkedSort(arr, compareFn, callback) {
  const CHUNK = 1000;
  let offset = 0;

  function sortChunk() {
    const end = Math.min(offset + CHUNK, arr.length);
    for (let i = offset; i < end; i++) {
      for (let j = i + 1; j < end; j++) {
        if (compareFn(arr[i], arr[j]) > 0) {
          [arr[i], arr[j]] = [arr[j], arr[i]];
        }
      }
    }
    offset = end;
    if (offset < arr.length) {
      setImmediate(sortChunk);
    } else {
      callback(arr);
    }
  }

  setImmediate(sortChunk);
}

chunkedSort(
  [5, 3, 8, 1, 9, 2, 7, 4, 6],
  (a, b) => a - b,
  (result) => {
    console.log('Sorted:', result);
  }
);
▶ 试一试
维度 阻塞代码 非阻塞代码
计算方式 同步循环一次跑完 拆分成小片,用 setImmediate 轮流执行
事件循环 卡死,其他请求等待 每片之间让出控制权,其他回调可执行
响应时间 累积延迟 分散延迟,用户可接受
CPU 利用 单核 100% 突发 分散使用,留出 I/O 时间
适用场景 极短计算 长计算、高并发服务


7. 综合示例:事件循环执行顺序验证脚本

以下脚本验证 setTimeout → process.nextTick → setImmediate → Promise.then → I/O callback 的完整执行顺序。

JAVASCRIPT
const fs = require('fs');

console.log('--- Script start ---');

setTimeout(() => {
  console.log('1. setTimeout (macrotask - timers)');
}, 0);

setImmediate(() => {
  console.log('2. setImmediate (macrotask - check)');
});

process.nextTick(() => {
  console.log('3. process.nextTick (microtask - highest)');
});

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

fs.readFile(__filename, () => {
  console.log('5. I/O callback (poll phase)');

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

  setImmediate(() => {
    console.log('7.   inner setImmediate');
  });

  process.nextTick(() => {
    console.log('8.   inner nextTick');
  });

  Promise.resolve().then(() => {
    console.log('9.   inner Promise.then');
  });
});

console.log('--- Script end ---');
TEXT 📖 仅展示
--- Script start ---
--- Script end ---
3. process.nextTick (microtask - highest)
4. Promise.then (microtask)
1. setTimeout (macrotask - timers)
2. setImmediate (macrotask - check)
5. I/O callback (poll phase)
8.   inner nextTick
9.   inner Promise.then
7.   inner setImmediate
6.   inner setTimeout

执行顺序解析

  1. 同步代码先执行,输出 Script start / Script end
  2. 当前阶段结束后清空微任务:nextTick → Promise.then
  3. 进入 timers 阶段:setTimeout
  4. 进入 check 阶段:setImmediate
  5. I/O 完成后在 poll 阶段执行回调
  6. I/O 回调结束后清空微任务:inner nextTick → inner Promise.then
  7. poll 结束进入 check:inner setImmediate
  8. 下一轮 timers:inner setTimeout

❓ 常见问题

Q setTimeout(fn, 0) 是真的 0ms 后执行吗?
A 不是。浏览器和 Node.js 都有最小延迟(约 1ms),且必须等到事件循环进入 timers 阶段才会执行,实际等待时间受当前阶段影响。
Q process.nextTick 和 Promise.then 谁先执行?
A process.nextTick 先执行。微任务队列中 nextTick 优先级最高,每次都会先清空 nextTick 队列再清空 Promise 队列。
Q 为什么 I/O 回调中 setImmediate 比 setTimeout 先执行?
A I/O 回调在 poll 阶段执行,结束后直接进入 check 阶段(setImmediate),然后才到下一轮 timers 阶段(setTimeout),所以在 I/O 上下文中 setImmediate 必定先于 setTimeout。
Q 事件循环是单线程还是多线程?
A JavaScript 执行是单线程的,但底层 libuv 有线程池(默认 4 线程)处理 DNS 查询、文件 I/O 等操作,网络 I/O 由操作系统内核完成,完成后通知事件循环。
Q 如何避免阻塞事件循环?
A 将长计算拆分成小片用 setImmediate 分步执行;使用 Worker Threads 将 CPU 密集任务移到子线程;用异步 I/O 代替同步 I/O;用流(Stream)分块处理大数据。
Q process.nextTick 可以无限递归吗?
A 不建议。Node.js 有 process.maxTickDepth 默认值限制,递归 nextTick 会阻止事件循环进入下一阶段,导致 I/O 饥饿。应该用 setImmediate 代替递归 nextTick。

📖 小节


📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏