nodeJS-eventLoop, 面试必备

eventLoop 是前端面试的常见问题,特别是中/高级前端开发工程师,希望能帮助到你,如有错误还请指点,必更正

浏览器的eventLoop 我们就不说了,之前有一篇文章专门来介绍了浏览器eventLoop

nodeJS的eventLoop, 基于 libuv 库,专注于 I/O 性能(文件、网络、子进程等)。浏览器的核心目标是保证 UI 的流畅响应。nodeJs的事件循环核心目标是高效处理非阻塞 I/O(尤其是文件、网络),

nodeJS的eventLoop 是分阶段性的,不像浏览器中的区分宏任务和微任务的执行

nodeJS的eventLoop中每个阶段中执行的也包含宏任务和微任务

Node.js 事件循环 (基于 libuv)

  • 架构基础: 基于 libuv 库,专注于 I/O 性能(文件、网络、子进程等)。

  • 阶段 (Phases): Node.js 事件循环被明确划分为几个顺序执行 的阶段。每个阶段都有一个先进先出(FIFO)的回调队列。当事件循环进入某个阶段时,它将执行该阶段队列中所有的 回调(直到达到系统相关的限制数量或队列为空),然后才会移动到下一个阶段。注意:process.nextTick() 和微任务队列的执行时机特殊,见下文。

  • 主要阶段 (按顺序):

    1. Timers (定时器阶段): 执行 setTimeout()setInterval() 中到期回调。

    2. Pending Callbacks (待定回调阶段): 执行某些系统操作(如 TCP 错误)的回调。

    3. Idle, Prepare (闲置、准备阶段): 仅 libuv 内部使用。

    4. Poll (轮询阶段 - 核心阶段):

      • 计算阻塞时间: 计算应该阻塞多久以等待新的 I/O 事件(基于下一个定时器的时间)。

      • 处理 I/O 事件: 执行几乎所有 I/O 相关的回调(文件读取、网络请求、用户自定义事件等)。如果轮询队列不为空,执行队列中的所有回调直到队列为空或达到系统限制。如果轮询队列为空:

        • 如果 setImmediate() 调度了回调,则结束 Poll 阶段,进入 Check 阶段执行这些回调。
        • 如果 没有 setImmediate() 回调,则事件循环将在 Poll 阶段阻塞等待新的 I/O 事件到达并立即执行它们的回调。
    5. Check (检查阶段): 执行 setImmediate() 设置的回调。

    6. Close Callbacks (关闭回调阶段): 执行关闭事件的回调(如 socket.on('close', ...))。

  • process.nextTick() 和微任务队列:

    • process.nextTick() 不属于事件循环的任何阶段。 它拥有一个独立的队列。在当前操作(无论事件循环处于哪个阶段)完成后、事件循环继续到下一个阶段之前,Node.js 会清空整个 nextTick 队列。 这意味着 nextTick 的优先级高于 微任务队列。递归调用 nextTick 可能导致 I/O 饥饿(因为事件循环一直被 nextTick 打断,无法进入 Poll 阶段处理 I/O)。
    • 微任务队列 (Microtask Queue): 包含 Promise.then/catch/finallyqueueMicrotask在事件循环的每个阶段(Timers, Pending, Poll, Check, Close)切换之前,Node.js 都会清空整个微任务队列(包括该阶段执行过程中产生的微任务)。 注意:虽然也是在阶段切换前执行,但它的优先级低于 nextTick 队列(先清空 nextTick 队列,再清空微任务队列,然后进入下一阶段)。

3. Node.js 与 浏览器事件循环的最大区别

  • 最大区别:架构目标与阶段划分

    • 浏览器: 核心目标是保证 UI 的流畅响应。它没有明确的阶段划分,主要区分宏任务和微任务,微任务在每次宏任务执行后、渲染前被彻底清空。渲染时机是流程的一部分。
    • Node.js: 核心目标是高效处理非阻塞 I/O(尤其是文件、网络)。它采用明确的、分阶段(Timers -> Pending -> Poll -> Check -> Close)的模型 。每个阶段处理特定类型的 I/O 回调。微任务 (Promise) 和 process.nextTick 的执行时机被严格定义在每个阶段切换的间隙,且 nextTick 优先级高于微任务。 Node.js 没有内置的 UI 渲染概念。
  • 其他关键区别:

    • setImmediate vs setTimeout(..., 0)

      • 在浏览器中,setTimeout(..., 0) 是常见的"尽快"执行方式(但仍是宏任务)。setImmediate 非标准。

      • 在 Node.js 中:

        • setImmediate() 设计在 Check 阶段执行。
        • setTimeout(..., 0)Timers 阶段执行。
        • 主模块/I/O 回调 中调用时,它们的执行顺序是不确定的(受进程性能影响)。
        • 同一个事件循环阶段(如 Poll 阶段)内 调用时,setImmediate 总是先于 setTimeout(..., 0) 执行(因为 Check 在 Timers 之后)。
    • process.nextTick 这是 Node.js 特有的机制,优先级极高(在当前操作后立即执行,甚至在微任务之前),浏览器中没有直接对应物。queueMicrotask 更接近浏览器的微任务行为。

    • I/O 类型: Node.js 需要处理更底层的、多样的 I/O(文件系统、网络套接字、子进程),而浏览器主要处理 DOM、网络请求(fetch/XMLHttpRequest)、用户交互事件等基于 Web API 的 I/O。

    • 渲染: 浏览器事件循环天然包含 UI 渲染步骤。Node.js 事件循环与此无关。

区别:

浏览器事件循环围绕宏任务/微任务划分和UI渲染优化,微任务在宏任务后立即全部执行。Node.js事件循环围绕libuv的明确I/O处理阶段(Timers, Poll, Check等)优化,微任务和nextTick在阶段切换间隙执行,且nextTick优先级最高。目标不同导致结构差异巨大:浏览器保UI,Node保I/O。

我之前一直有一个疑问?

在 同一个事件循环阶段(如 Poll 阶段)内 调用时,setImmediate 总是先于 setTimeout(..., 0) 执行(因为 Check 在 Timers 之后)。check在timers之后,不应该setImmediate在setTimeout后面吗,怎么是先于呢

原因在于,同一个事件循环阶段,进入timer阶段,执行的是上一次的宏任务setTimeout, 本次事件循环在poll阶段产生的宏任务,setTimeout或者setImmediate, 分别在本次的check阶段和下一次的事件循环的timer阶段执行,所以 setImmediate 比setTimeout 先执行,不知这么理解是否正确

相关推荐
Nan_Shu_61416 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#24 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界40 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript
学历真的很重要1 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架