javascript 是一门单线程语言,为了协调同步任务与异步任务,它引入了事件循环(Event Loop)的机制。然而,javascript 的运行环境主要有两种——浏览器和 Node.js,它们的事件循环实现方式有显著差异。理解这些差异对于编写可靠的跨环境代码至关重要。
宏观架构差异
浏览器事件循环
浏览器的事件循环由宿主环境(浏览器)实现,主要协调以下组成部分:
- 调用栈(Call Stack):执行同步代码。
- 宏任务队列(MacroTask Queue):存放 setTimeout、setInterval、I/O、UI 渲染等回调 。
- 微任务队列(MicroTask Queue):存放 Promise.then、MutationObserver 等回调 。
- 渲染引擎:负责页面绘制,与事件循环协同工作 。
Node.js 事件循环
Node.js 的事件循环由 libuv 库 实现,它包含多个阶段(phase),每个阶段都有自己的任务队列 。Node.js 的事件循环结构更复杂,因为它需要处理底层操作系统的 I/O 操作。
| 维度 | 浏览器事件循环 | Node.js 事件循环 |
|---|---|---|
| 核心实现 | 浏览器宿主环境 | libuv 库 |
| 阶段划分 | 简化为宏任务队列 + 微任务队列 | 细分为 6 个阶段(timers、I/O、poll、check 等) |
| 任务粒度 | 每个宏任务队列执行一个任务就切换 | 每个阶段会清空当前阶段的整个队列 |
| 微任务执行时机 | 每个宏任务执行后清空微任务 | 每个阶段之后清空微任务(Node 11+ 与浏览器对齐) |
Node.js 事件循环的六个阶段
Node.js 的事件循环按照以下顺序循环执行 :
1 | ┌───────────────────────────┐ |
每个阶段结束后,Node.js 会先清空微任务队列(process.nextTick 和 Promise.then),然后进入下一阶段 。
核心差异详解
任务队列结构
- 浏览器:只有两个任务队列(宏任务、微任务)。
- Node.js:每个阶段都有独立的队列,外加两个微任务队列(nextTick 队列和 Promise 队列)。
任务执行粒度
- 浏览器:从宏任务队列中取出第一个任务执行,然后清空微任务队列,接着检查是否需要渲染,再取下一个宏任务 。
- Node.js:进入一个阶段后,会清空该阶段队列中的所有任务,然后才执行微任务,再进入下一阶段 。
微任务执行时机
- 浏览器:每个宏任务执行完毕后清空微任务 。
- Node.js:
– Node 11 之前:在阶段切换时才清空微任务,可能导致一个阶段内积累大量微任务 。
– Node 11 及之后:在每个宏任务执行后也会清空微任务,行为与浏览器基本对齐 。
特殊 API
- 浏览器独有:requestAnimationFrame、MutationObserver 。
- Node.js 独有:setImmediate、process.nextTick 。
– setImmediate 在 check 阶段执行 。
– process.nextTick 不属于事件循环的任何阶段,而是在每个阶段结束后立即执行,优先级高于 Promise 。
代码示例验证差异
示例 1:setTimeout 与 setImmediate 的执行顺序
1 | <!-- Node.js 环境 --> |
示例 2:process.nextTick 与 Promise 的优先级
1 | process.nextTick(() => console.log('nextTick')); |
示例 3:Node 11 前后的行为差异
1 | setTimeout(() => { |
总结与建议
| 对比维度 | 浏览器事件循环 | Node.js 事件循环 |
|---|---|---|
| 核心模型 | 宏任务 + 微任务 | 多阶段 + 微任务 |
| 微任务时机 | 每个宏任务后 | 每个阶段后(Node 11+ 也支持每个宏任务后) |
| 特殊 API | requestAnimationFrame | setImmediate, process.nextTick |
| 复杂程度 | 相对简单 | 更复杂,涉及系统 I/O |
编写跨环境代码的建议
- 避免依赖执行顺序:尽量不要编写依赖 setTimeout 与 setImmediate 执行顺序的业务代码。
- 使用 queueMicrotask:如果需要微任务,优先使用标准 API queueMicrotask 而不是 process.nextTick。
- 了解版本差异:Node 11+ 的事件循环与浏览器行为更接近,但仍需注意 nextTick 等特有 API 的存在 。
- 利用 I/O 回调稳定顺序:在 Node.js 中,如果需要在 I/O 之后立即执行任务,setImmediate 是最可靠的选择 。