NodeJS的事件循环

为什么需要事件循环?

Node.js 是单线程运行的,但它却可以处理成千上万个并发连接。它之所以能做到这点,是因为它使用了事件驱动、非阻塞的 I/O 模型

关键点:

  • 单线程负责执行 JavaScript 代码。
  • 异步任务 (如定时器、网络请求、文件 I/O)通过回调函数在事件循环中排队执行。
  • 不会像传统阻塞式模型那样等待任务完成。

事件循环的核心结构

Node.js 的事件循环是基于 libuv 实现的。libuv 是一个跨平台的异步 I/O 库,为事件循环、线程池、网络等提供支持。 事件循环可以理解为一个不断执行的阶段轮转系统

js 复制代码
while (there are events in the event queue) {
  execute next event from the queue
}

但在 Node.js 中,事件循环是分阶段执行的,每个阶段都有各自的任务队列。

Node.js 的事件循环大致可以分为以下几个阶段:

  1. timers :执行 setTimeoutsetInterval 的回调。
  2. pending callbacks:执行某些系统操作(如 TCP 错误)的回调。
  3. idle, prepare:内部使用阶段(不暴露给开发者)。
  4. poll:获取新的 I/O 事件,执行与 I/O 相关的回调(如读取文件、网络请求等)。
  5. check :执行 setImmediate 的回调。
  6. close callbacks :执行如 socket.on('close', ...) 这种回调。

图示:

每次进入一个阶段,Node 会从对应的任务队列中取出所有任务并执行,执行完后进入下一个阶段。

NodeJS事件循环各阶段解读

Timers阶段

Timers 阶段,Node.js 会执行所有已到期的定时器回调(例如 setTimeout()setInterval() 的回调)。如果设置的定时器时间已经到期,这些回调会被推入执行队列,等待下一次事件循环执行。

Pending Callbacks阶段

处理一些底层 I/O 操作的回调。

Idle、Prepare阶段

用于内部操作,通常不需要关注。

Poll阶段

在 Node.js 的事件循环中,Poll 阶段 是一个非常关键的阶段,它负责处理大部分的 I/O 操作 。在事件循环的这个阶段,Node.js 会检查 I/O 操作是否完成并执行相应的回调函数。如果没有待处理的 I/O 操作,Poll 阶段会决定是否需要进入下一阶段,或是继续等待新的事件。

Poll 阶段的主要作用

  1. 执行 I/O 回调

    • Poll 阶段,Node.js 会处理已经完成的 I/O 操作的回调,比如网络请求的响应、文件读取的完成、数据库查询结果的返回等。
    • 如果有挂起的 I/O 操作回调,Node.js 会在这个阶段执行它们。
  2. 检查是否有 I/O 事件

    • 如果在 Poll 阶段没有需要执行的回调,Node.js 会检查 I/O 事件队列。如果有新的 I/O 事件(比如文件系统操作、网络操作等),事件循环会继续停留在这个阶段,直到有新的事件被加入队列,当然其它阶段的回调需要被执行时,会再进行一次循环,比如Timers阶段计时器延时到期,Check阶段中存在setImmediate回调。
  3. 决定是否阻塞或继续执行

    • 如果没有 I/O 回调需要执行且没有新的 I/O 事件,Poll 阶段会决定是否进入 Check 阶段 或者继续等待新的 I/O 事件。默认情况下,Node.js 会选择阻塞在 Poll 阶段,直到有新的 I/O 事件。
  4. 执行轮询超时

    • Poll 阶段还会管理轮询超时行为。这个行为涉及到在没有 I/O 操作完成时,等待新的 I/O 操作的发生。实际上,这个阶段会维持阻塞状态,直到有 I/O 操作被触发,或者 Poll 阶段时间到达上限,进入下一个阶段。

Check阶段

执行 setImmediate 的回调。

close callbacks阶段

执行如 socket.on('close', ...) 这种回调。

对于上述所有NodeJS事件循环阶段,我们只需要关心TimersPollcheck三个阶段即可。

微任务队列

在NodeJS事件循环中所有任务队列都属于宏任务,而这些宏任务队列在执行之前都需要清空微任务队列。

微任务队列是什么?

微任务是指 process.nextTick()Promise.then() 产生的任务。Node.js 中有两个微任务队列:

  1. process.nextTick() 队列(优先级高于 Promise)
  2. Promise.then/.catch/.finally() 回调

举例:

js 复制代码
setTimeout(() => {
    console.log('setTimeout');
}, 0);

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

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

输出顺序

bash 复制代码
nextTick
promise
setTimeout
setImmediate

解释

  1. 同步代码完成后,先清空 process.nextTick 队列 → 输出 nextTick
  2. 然后清空 Promise 微任务队列 → 输出 promise
  3. 然后进入 timers 阶段,执行 setTimeout → 输出 setTimeout

总结

行为 是否会在每个阶段前清空微任务?
清空 process.nextTick 队列 ✅ 是的(优先级最高)
清空 Promise 微任务队列 ✅ 是的(在 nextTick 之后)
每个阶段开始前是否清空微任务? ✅ 是的,确保所有微任务都处理完才进入下一个阶段

这也是为什么在 Node.js 中,微任务(尤其是 process.nextTick)可以"插队"于事件循环的各个阶段之间,被用作高优先级的异步处理机制。

总结

在 Node.js 程序执行时,会从入口函数开始执行同步代码。遇到异步任务时,这些任务会被注册,随后进入事件循环(Event Loop)。事件循环会依次执行各个阶段中的任务队列。

在每个阶段执行完后,Node.js 会立即清空当前的微任务队列(如 Promise.thenprocess.nextTick)。

其中最核心的阶段是 Poll 阶段

  • 如果有 I/O 回调需要执行,就会执行这些回调;
  • 如果没有待执行的回调,但有挂起的 I/O 操作,事件循环会在此阶段 阻塞等待 , 这个阻塞并不是说一直卡在这,而是当前其它阶段有待执行的任务时,会先去执行这些任务,然后回到Poll阶段继续阻塞等待;
  • 如果既没有回调也没有挂起的 I/O,且后续阶段没有待执行的任务(如 setImmediate),则程序会退出。

常见问题

1. setTimeout的延时时间在严格意义上来说是不是取不到0?

是的,setTimeout 的延迟时间在严格意义上来说 不能 取到 0 毫秒。虽然你可以传递 0 作为延迟时间,但它并不是立即执行的。原因在于 JavaScript 的事件循环机制和最小时间延迟的限制。

  1. 事件循环的机制: JavaScript 是单线程执行的,所有的异步操作(如 setTimeoutPromise 等)都依赖于事件循环。即使你指定 0 毫秒延迟,回调函数也不会在当前执行栈中立即执行,而是会被放入事件队列,等待当前任务栈清空后执行。
  2. 最小延迟: 大多数浏览器(特别是现代浏览器)对 setTimeout 有一个最小延迟限制。即便你传递 0 作为延迟,浏览器通常会将延迟时间设置为 4毫秒1毫秒 。这意味着即使你指定 0,回调也不会立即执行,而是最少会等几毫秒才执行。
  3. 渲染与系统负载: 由于浏览器的渲染机制及系统的负载,实际执行的时间可能比你期望的要稍微延迟一些,尤其是在高负载情况下。

2. setTimeout(0)setImmediate哪个会先执行?

在 Node.js 的事件循环中,setTimeout(0)setImmediate() 的执行顺序不是固定的,取决于调用时的上下文环境。以下是详细解释:

  1. 主模块中的执行顺序(不确定)
javascript 复制代码
// 示例代码
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
  • 结果可能随机 :可能是 setTimeout 先输出,也可能是 setImmediate 先输出。

  • 原因

    • 事件循环启动需要时间。如果准备阶段耗时超过 1ms,定时器阈值(1ms)已满足,则先执行 setTimeout
    • 否则事件循环直接进入 check 阶段,先执行 setImmediate
  1. I/O 回调中的执行顺序(固定)
javascript 复制代码
const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => console.log('setTimeout'), 0);
  setImmediate(() => console.log('setImmediate'));
});
  • 结果固定setImmediate 总是先于 setTimeout

  • 原因

    • I/O 回调在 poll 阶段执行。
    • setImmediate 会在当前 poll 阶段后的 check 阶段立刻执行
    • setTimeout 需等待下一次循环的 timers 阶段执行。
相关推荐
奕辰杰3 小时前
关于npm前端项目编译时栈溢出 Maximum call stack size exceeded的处理方案
前端·npm·node.js
yzzzzzzzzzzzzzzzzz13 小时前
node.js之Koa框架
node.js
Java陈序员14 小时前
轻松设计 Logo!一款 Pornhub 风格的 Logo 在线生成器!
vue.js·node.js·vite
gongzemin16 小时前
使用Node.js开发微信第三方平台后台
微信小程序·node.js·express
JavaDog程序狗1 天前
【软件环境】Windows安装NVM
前端·node.js
自学也学好编程1 天前
【工具】NVM完全指南:Node.js版本管理工具的安装与使用详解
node.js
Moment1 天前
调试代码,是每个前端逃不过的必修课 😏😏😏
前端·javascript·node.js
萌萌哒草头将军1 天前
Prisma ORM 又双叒叕发布新版本了!🚀🚀🚀
前端·javascript·node.js
zyfts2 天前
手把手教学Nestjs对excel的增删改查
前端·node.js
星空下的曙光2 天前
nodejs项目中常用的npm包及分类
node.js