事件循环

browser_principles/event_loop2026-10-16 00:22:14

JavaScript 的事件循环(Event Loop)是运行时协调任务执行的机制。同一执行代理中的 JavaScript 通常一次执行一段代码;异步操作允许等待期间处理其他工作,但耗时的同步计算仍会阻塞该执行线程。以下主要介绍浏览器环境,Worker 等执行代理可有各自的执行环境。

事件循环的基本概念

调用栈(Call Stack)

调用栈是一个 LIFO(后进先出)结构,用于存储函数调用。JavaScript 引擎在调用函数时会将其压入调用栈,函数执行完毕后会将其弹出调用栈。

消息队列(Message Queue)

消息队列用于存储待处理的任务。把它理解为 FIFO(先进先出)队列有助于入门,但浏览器存在多个任务来源及任务队列,并非所有任务都进入一个全局 FIFO 队列。事件循环在适当时机选择可运行的任务,再执行相关代码。

微任务和宏任务

介绍事件循环时,通常区分微任务(Microtasks)和宏任务(Macrotasks;HTML 标准称为 task)。所谓“微任务优先”,是指在微任务检查点清空微任务队列后才继续后续工作,不是微任务可以随时打断正在执行的同步代码。

  • 微任务:包括 Promise 回调、MutationObserver 回调等。
  • 宏任务:包括 setTimeout、setInterval、I/O 操作等。

事件循环的工作流程

浏览器事件循环选择可运行任务并执行相关代码,在微任务检查点处理微任务,并在适当的渲染机会更新页面。调用栈为空是理解执行时机的重要条件,但不能单凭这一条件概括全部调度规则。

  1. 同步任务:所有同步任务在调用栈中按顺序执行。
  2. 异步任务:定时器、网络和 DOM 事件等按对应宿主 API 的规则调度后续工作;Promise 的反应回调进入微任务队列,不能把所有异步回调都视为同一种队列任务。
  3. 事件循环:执行任务后,在微任务检查点处理队列中的微任务,包括处理期间新加入的微任务;随后继续调度。浏览器在适当机会渲染,并非每执行一个任务都必然绘制一帧。

执行顺序示例

下面作为浏览器中的一段普通脚本执行:

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
(async () => {
  console.log("D");
  await 0;
  console.log("E");
})();
console.log("F");
// A D F C E B

async 函数在第一个 await 之前同步执行,之后的继续执行进入微任务。setTimeout 的延迟不保证精确执行时间。持续追加微任务也可能延迟后续任务和渲染;把计算放进 Promise 回调不会自动转到其他线程。

Node.js 事件循环

Node.js 的事件循环和浏览器的事件循环有一些区别。参考 Node.js 事件循环。

阶段划分

Node.js 的事件循环分为多个阶段,包括定时器阶段、待定回调阶段、闲置准备阶段、轮询阶段、检查阶段和关闭回调阶段。

process.nextTick

Node.js 提供了一个特殊的 process.nextTick 队列,通常在当前 JavaScript 操作完成后、事件循环继续推进前处理。它不是普通的事件循环阶段;递归追加 nextTick 可能阻碍 I/O。与 Promise 微任务的相对顺序还需结合执行上下文判断,例如 CommonJS 与 ES module 的顶层执行可能有不同表现。

setImmediate

Node.js 提供了 setImmediate,它会在检查阶段执行。

参考资料

本页目录