事件循环(Event Loop)

浏览器事件循环和 Node.js 的区别

浏览器和 Node.js 都使用事件循环(Event Loop)来处理异步任务,但由于运行环境不同,它们的事件循环机制在架构设计、宏任务与微任务处理、任务来源和模块支持等方面存在明显差异。

一、浏览器事件循环机制

浏览器的事件循环遵循 HTML5 标准,基本结构如下:

  1. 执行顺序
    同步任务(调用栈)
    微任务队列(Microtasks):如 Promise.then、MutationObserver
    宏任务队列(Macrotasks):如 setTimeout、setInterval、requestAnimationFrame
  2. 典型任务来源
    任务类型 示例
    宏任务 setTimeout、setInterval、message、UI 渲染、XHR onload
    微任务 Promise.then、queueMicrotask、MutationObserver
  3. 特点
    每执行一个宏任务,立即清空所有微任务。
    浏览器事件循环中含有 UI 渲染阶段,微任务清空后才允许渲染。

二、Node.js 的事件循环机制

Node.js 基于 libuv 库实现自己的事件循环,主要包含 6 个阶段,不完全等同于浏览器模型。

  1. Node.js 事件循环阶段
    timers:处理 setTimeout、setInterval
    pending callbacks:处理某些 I/O 的回调
    idle, prepare:仅内部使用
    poll:检索 I/O 事件
    check:处理 setImmediate
    close callbacks:处理 close 事件,如 socket.on('close')

nodejs-eventloop.png

每个阶段之间都会执行微任务队列。

  1. 微任务来源
    process.nextTick()(优先级最高,不属于微任务队列,是独立队列)
    Promise.then()(真正的微任务)

  2. 特点
    process.nextTick 比 Promise.then 更早执行。
    没有 UI 渲染阶段(非浏览器)。
    setImmediate 与 setTimeout(..., 0) 行为不同。

三、主要区别对比

项目浏览器Node.js
环境有 UI 渲染无 UI 渲染
宏任务示例setTimeout, setIntervalsetTimeout, setImmediate
微任务队列Promise, MutationObserverPromise, process.nextTick(优先)
微任务执行时机每个宏任务后执行所有微任务每个阶段后执行所有微任务(先执行 nextTick)
特殊队列无 nextTickprocess.nextTick 队列优先于微任务
底层实现浏览器厂商自研libuv 实现多平台 I/O

总结记忆口诀:浏览器关注 UI,先宏后微;Node 有 Tick,分阶段处理。

nodejs-browser-diff.png

宏任务(Macrotasks)

定义:

宏任务是事件循环中调度和执行的主要任务类型,每个宏任务代表一个大的、独立的执行单元。它们通常对应于操作系统或者浏览器层面触发的异步事件,比如计时器、用户交互、网络请求回调等。

特点:

每个宏任务之间,JavaScript 执行上下文是清空的,即宏任务执行时是一个全新的执行上下文。
宏任务执行完后,才会触发微任务队列的清空。
浏览器的 UI 渲染也发生在宏任务完成后。
宏任务队列中的任务是顺序执行的,一个宏任务执行完毕后才会执行下一个。

示例:

setTimeout、setInterval、setImmediate(Node.js)、I/O 回调、UI 渲染等。

微任务(Microtasks)

定义:

微任务是在当前宏任务执行完毕后立即执行的小任务,设计目的是允许开发者将某些任务安排得比下一个宏任务更早执行,从而对事件循环做更细粒度的控制。

特点:

微任务队列会在每个宏任务执行结束后立即清空。
微任务在执行时,JavaScript 执行上下文仍然处于当前宏任务的上下文之后,渲染之前。
微任务可以产生新的微任务,因此执行微任务时会持续清空微任务队列,直到队列为空。
微任务优先级高于宏任务。

示例:

Promise.then / catch / finally 回调、MutationObserver、queueMicrotask、Node.js 的 process.nextTick(特殊微任务,优先级最高)。

汇总表格

类型具体任务特点简介
宏任务- setTimeout任务队列较大,调度周期较长。宏任务会在当前执行栈清空后执行。
- setInterval定时重复执行的宏任务。
- setImmediate(Node.js)类似于setTimeout(fn, 0),但优先级更高(Node环境)。
- I/O操作回调网络请求、文件读取等异步操作的回调。
- UI渲染浏览器的渲染工作,宏任务结束后浏览器会进行UI更新。
- messageChannel消息事件使用MessageChannel API传递的宏任务。
微任务- Promise.then微任务会在当前宏任务执行完毕后、渲染之前执行,优先级高于宏任务。
- Promise.catchthen,是Promise的错误处理微任务。
- Promise.finallyPromise的最终处理微任务。
- MutationObserverDOM变动监听,微任务队列中执行。
- queueMicrotask直接创建微任务,优先级同Promise微任务。
- process.nextTick(Node.js)特殊微任务,优先级最高,会在其他微任务之前执行,用于立即执行回调。

事件循环(Event Loop)中的执行流程简述

  1. 执行一个宏任务,比如执行一个 setTimeout 回调或者页面的主代码。
  2. 执行所有微任务,直到微任务队列为空。
  3. 进行一次 UI 渲染(如果环境支持,比如浏览器)。
  4. 开始下一个宏任务,重复上述流程。

示例代码

setTimeout(() => {
  console.log('setTimeout');
}, 0);
 
setImmediate(() => {
  console.log('setImmediate');
});
 
Promise.resolve().then(() => {
  console.log('Promise');
});
 
process.nextTick(() => {
  console.log('nextTick');
});

输出顺序可能是:

nextTick
Promise
setTimeout
setImmediate

理论意义总结

  • 宏任务代表“较重”的异步操作单位,是事件循环调度的核心。
  • 微任务是微调机制,允许任务在当前宏任务结束后立即执行,确保异步任务可以尽快处理,避免事件循环切换过于频繁带来的性能问题。
  • 这种划分使得 JavaScript 能保持单线程顺序执行的同时,灵活高效地处理异步事件。