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()
和微任务队列的执行时机特殊,见下文。 -
主要阶段 (按顺序):
-
Timers (定时器阶段): 执行
setTimeout()
和setInterval()
中到期回调。 -
Pending Callbacks (待定回调阶段): 执行某些系统操作(如 TCP 错误)的回调。
-
Idle, Prepare (闲置、准备阶段): 仅 libuv 内部使用。
-
Poll (轮询阶段 - 核心阶段):
-
计算阻塞时间: 计算应该阻塞多久以等待新的 I/O 事件(基于下一个定时器的时间)。
-
处理 I/O 事件: 执行几乎所有 I/O 相关的回调(文件读取、网络请求、用户自定义事件等)。如果轮询队列不为空,执行队列中的所有回调直到队列为空或达到系统限制。如果轮询队列为空:
- 如果
setImmediate()
调度了回调,则结束 Poll 阶段,进入 Check 阶段执行这些回调。 - 如果 没有
setImmediate()
回调,则事件循环将在 Poll 阶段阻塞等待新的 I/O 事件到达并立即执行它们的回调。
- 如果
-
-
Check (检查阶段): 执行
setImmediate()
设置的回调。 -
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/finally
、queueMicrotask
。在事件循环的每个阶段(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
vssetTimeout(..., 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 先执行,不知这么理解是否正确