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 阶段执行。
相关推荐
hweiyu006 小时前
Node.js 简介(附电子学习资料)
node.js
盛夏绽放6 小时前
Node.js 项目启动命令大全 (形象版)
node.js·有问必答
锋君9 小时前
node.js使用websockify代理VNC代理使用NoVNC进行远程桌面实现方案
服务器·node.js·novnc
Q_Q51100828510 小时前
python题库及试卷管理系统
开发语言·spring boot·python·django·flask·node.js·php
前端服务区10 小时前
GitLab自动化部署的流程
node.js
草履虫建模10 小时前
Vue3 + Spring Boot 前后端分离项目搭建
java·前端·css·vue.js·spring boot·后端·node.js
爱分享的程序员11 小时前
Node.js特训专栏-实战进阶:4.Express中间件机制详解
前端·javascript·node.js
盛夏绽放11 小时前
Node.js 文件上传方案终极对决:Multer vs Connect-Multiparty
node.js·编辑器·vim·有问必答
babicu12311 小时前
Node.js
前端·webpack·node.js