Node.js 简介

事件循环与异步机制

理解 Node.js 单线程与事件循环的工作原理,掌握同步代码与各类异步 API 的执行顺序。

🎯 引言

学完这篇文章,你能说清 Node.js 单线程为什么还能高效处理大量请求,理解事件循环的基本工作流程,并能正确预测 setTimeoutsetImmediatePromise 混合代码的输出顺序。


🧱 Node.js 是单线程的吗

执行 JavaScript 代码的主线程只有一条,这是事实。但 Node.js 靠“事件循环 + 非阻塞 I/O”这套机制,一条线程就能同时应付大量请求。

用餐厅打比方:一个服务员不会站在某桌旁边干等菜做好,而是点完单立刻去服务下一桌,等厨房喊“菜好了”再回来上菜。Node.js 也一样:读写文件、查数据库这些耗时操作交给系统去办,主线程继续处理别的请求,事情办完了再通过回调通知主线程来取结果。


🧱 为什么这样设计

服务器程序的大部分时间都花在等待 I/O 上:等文件读完、等数据库返回、等网络响应。如果每个请求都阻塞等待,线程就会大量闲置。

非阻塞的设计把等待时间利用了起来:一个请求在等数据库时,主线程可以去处理下一个请求。所以 Node.js 非常适合 I/O 密集型场景,比如接口服务、网关、实时聊天。

反过来,CPU 密集型的大计算(比如超大规模的循环计算)会一直占用主线程,把整个事件循环堵住,所有请求都得排队。这是 Node.js 的能力边界,了解即可,不要在服务里写超大同步循环。

🧱 事件循环的简化模型

不用深入底层源码,初学者记住这个主流程就够用了:

  1. 同步代码:按书写顺序,先全部执行完。
  2. 微任务process.nextTick 先执行,然后是 Promise.then
  3. 宏任务各阶段循环:timers 阶段执行到期的 setTimeout / setInterval,poll 阶段执行 I/O 回调,check 阶段执行 setImmediate。每完成一个阶段,都会回头清空微任务队列。
order.js
console.log('1. 同步代码'); // 第 1 步:同步先跑

process.nextTick(() => console.log('2. nextTick')); // 第 2 步:微任务里它优先

Promise.resolve().then(() => console.log('3. Promise')); // 第 3 步:微任务

setTimeout(() => console.log('4. setTimeout'), 0); // 第 4 步:timers 阶段

setImmediate(() => console.log('5. setImmediate')); // 第 5 步:check 阶段

⚡ 执行顺序实战

运行上面的代码,输出是:

1. 同步代码
2. nextTick
3. Promise
4. setTimeout
5. setImmediate

逐个解释:

  • 同步代码永远先执行,没有悬念。
  • process.nextTick 的优先级高于 Promise.then,它会在当前操作结束后、事件循环继续之前立刻执行。
  • setTimeout(fn, 0) 属于 timers 阶段,setImmediate 属于 check 阶段,timers 排在 check 前面,所以 4 在 5 前面。
setTimeout(fn, 0)setImmediate 的先后只在“主模块直接调用”时可能不稳定(受准备耗时影响)。把这段代码放进文件读取的回调里,setImmediate 就一定会先执行,因为 I/O 回调处于 poll 阶段,下一个阶段就是 check。面试遇到可以再深究,日常开发知道这个规律即可。

💡 给初学者的实用建议

  • 写异步代码优先用 async/await,它的可读性比嵌套回调和 .then 链好得多。
  • 不要在主线程写超大的同步循环,它会卡住所有请求。
  • 面试常考执行顺序题,掌握“同步 → nextTick → Promise → setTimeout → setImmediate”这条主线,就能应付常见题目。

🧾 小节总结

  • Node.js 的 JS 主线程是单线程,靠事件循环和非阻塞 I/O 处理并发。
  • 服务器大量时间在等 I/O,非阻塞设计把等待时间利用了起来,适合 I/O 密集型场景。
  • 执行顺序主线:同步代码 → process.nextTickPromise.thensetTimeoutsetImmediate
  • CPU 密集型计算会堵住事件循环,应避免在服务里执行。

❓ 知识问答

Q1:setTimeout(fn, 0) 是立刻执行吗?

不是。“0 毫秒”只是理论上尽快,它至少要等同步代码和微任务全部执行完,事件循环进入 timers 阶段才会执行。

Q2:setImmediate 和 setTimeout(fn, 0) 谁先执行?

主模块里直接调用时顺序不固定,受进程启动耗时影响;在 I/O 回调里调用时 setImmediate 一定先执行。日常开发不要依赖两者的相对顺序。

Q3:process.nextTick 和 Promise.then 谁优先?

process.nextTick 优先。事件循环每次继续之前,会先清空 nextTick 队列,再执行 Promise 微任务。

Q4:Node.js 既然是单线程,为什么还能同时处理很多请求?

因为耗时的 I/O 操作都交给了操作系统底层处理,主线程不阻塞等待,只负责调度和处理结果。请求量越大、I/O 占比越高,这个机制的优势越明显;具体能支撑多大的并发量,和你的代码质量、机器配置都有关系,需要实测验证。


🧪 小练习

不运行代码,先写出下面这段代码的输出顺序,再用 node 运行验证你的答案:

test.js
console.log('A');

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

Promise.resolve().then(() => {
    console.log('C');
    process.nextTick(() => console.log('D'));
});

process.nextTick(() => console.log('E'));

console.log('F');

// 请先在这里写下你预测的顺序,再运行验证

🎉 恭喜你已经理解 Node.js 的事件循环机制!下一篇我们学习 process 对象,掌握命令行参数和环境变量的用法。