事件循环如何工作?
未标记
考查重点:任务、微任务与渲染时机。同步栈先执行,任务完成做微任务检查点,再取后续任务/渲染机会;长同步任务会挡交互和计时器
本题目录
参考答案
核心回答
事件循环由运行环境协调任务的执行,让 JavaScript 在等待网络、定时器等操作时,不必一直阻塞着等待结果。以浏览器窗口主线程为例,当前 JavaScript 先执行;异步操作到达可处理的时机后,相关回调再被调度。
理解顺序时,要分清当前调用栈、任务(常称宏任务)和微任务。Promise 的反应回调、queueMicrotask 等进入微任务队列,在微任务检查点处理;定时器等任务则由事件循环调度。一次微任务执行时新加入的微任务,也会在这次检查点继续处理,直到队列清空。
不能简单记成“异步代码都在另一条线程里执行”,也不能认为“执行完一个任务就一定刷新一帧”。网络工作、回调执行和页面渲染分别由宿主机制协调。
任务、微任务分别是什么?
- 当前同步代码:函数调用会进入执行栈,普通代码、
Promise构造器里的执行函数都会立即执行。注册回调不等于回调已经运行。 - 任务:定时器到期后的回调、用户交互、网络相关的处理等,可以通过相应任务来源调度。浏览器可能维护多个任务队列,不能假设所有来源都在同一个全局 FIFO 队列中。
- 微任务:
Promise的then、catch等反应回调、queueMicrotask,以及MutationObserver的通知处理等。它们在指定检查点执行,不会抢占当前正在运行的同步代码。
一个典型过程是:执行当前任务中的代码,在相应检查点清空微任务,再继续调度可运行的任务。检查点不应机械理解成“只有整页脚本最后才发生”;宿主调用脚本结束等位置,也有对应规则。
看一段输出顺序
在浏览器中执行下面代码:
console.log("A");
setTimeout(() => {
console.log("T");
Promise.resolve().then(() => console.log("P2"));
}, 0);
new Promise((resolve) => {
console.log("E");
resolve();
}).then(() => {
console.log("P1");
queueMicrotask(() => console.log("M2"));
});
queueMicrotask(() => console.log("M1"));
console.log("B");
输出为:A → E → B → P1 → M1 → M2 → T → P2。
- 先执行同步部分,输出 A、E、B。
Promise构造器中的 E 是同步执行的;定时器只是注册,then的回调也还没执行。 - 此时已经排入的微任务依次是 P1、M1。执行 P1 时,M2 加到队尾,因此接着输出 M1、M2。
- 微任务清空后,定时器回调有机会运行,输出 T。它又加入 P2 微任务,所以随后输出 P2。
关键是按回调实际入队的时机推导。并不是源码里出现 Promise 这个词,就把整个构造过程都丢到微任务中。
setTimeout 的 0 毫秒为什么不等于立即执行?
延迟值表达的是达到可以调度的时间条件,不是精确执行时刻。即使设成 0,也要等当前代码和相应微任务处理,再受任务调度、浏览器限频等因素影响。
如果主线程正在执行一个很长的计算,计时已经到期也不能打断它去运行回调。因此,将长计算包进 setTimeout 只是推迟开始;回调一旦开始执行,仍可能阻塞页面。
微任务与页面渲染有什么关系?
浏览器根据渲染机会和页面状态安排更新,不承诺每执行一次任务就绘制一次。微任务检查点会持续处理新增微任务,因此递归加入微任务,可能让后续任务和渲染迟迟得不到机会。
所以 await Promise.resolve() 或反复 queueMicrotask 不能自动把大量计算变得不阻塞,也不能保证界面已经绘制。重计算可以考虑移到 Worker,或拆成有界工作并适当地让出执行机会;需要与绘制节奏协调的界面更新可使用 requestAnimationFrame,其回调本身也不应执行很长的计算。
Worker 有自己的执行环境与事件循环,不会直接操作窗口 DOM。这与主线程安排异步回调是不同的并发方式。
Node.js 也是同一套规则吗?
“同步执行、异步回调由宿主调度”的基本思想相通,但 Node.js 还有 libuv 的阶段,例如 poll 处理 I/O、check 执行 setImmediate,以及 process.nextTick 等机制。它没有浏览器页面渲染这一环,不能把浏览器的全部顺序原样套过去。
具体顺序还要看运行位置、模块形式和版本。例如 Node 官方文档说明,libuv 1.45.0(Node.js 20)调整了每轮事件循环中 timers 相对 poll 的位置;不能据此背一个跨版本、跨场景的 setTimeout(0) 与 setImmediate 固定先后顺序。
常见追问
JavaScript 单线程,为什么网络请求不会把页面完全卡住?
异步网络操作由宿主的网络机制处理,等待期间不需要主线程一直运行等待代码。完成后的回调仍要调度到相应环境执行;若回调本身计算很久,页面照样可能卡顿。
async/await 会新开线程吗?
不会。异步函数在遇到 await 前仍同步执行;await 后的继续执行通过 Promise 等机制安排,不会自动把 CPU 密集计算移到后台线程。
微任务优先级更高,是不是应该把任务都放进去?
不应该。它适合在当前处理结束后及时整理状态,但长微任务或不断新增微任务会推迟输入响应和绘制。应根据任务性质与耗时选择调度方式。
参考资料
- 国内:阮一峰:JavaScript 运行机制详解,再谈 Event Loop:借鉴执行栈、异步等待与回调的介绍,区分正文中宿主等待与回调执行;微任务和现代宿主规则按规范限定。
- 国内:网道:定时器:采用同步长任务与定时器的例子,解释正文中延迟值不是精确执行时刻,以及 0 毫秒定时器仍需等待调度。
- 国内:阮一峰:ES6 入门,Promise 对象:借鉴 Promise 构造器与反应回调的执行时机,解释正文示例的输出顺序。
- 国外:WHATWG HTML:Event loops:依据任务来源、多个任务队列、微任务检查点和渲染调度,解释正文中为何不能假定只有一个全局队列或每轮必绘制。
- 国外:Node.js:The Node.js Event Loop:介绍 libuv 阶段及定时器、I/O、nextTick 的运行背景,说明浏览器与 Node 的宿主规则为何需要分开讨论。
相关面经
6 篇科沃斯
助理开发工程实习生(AI 助手项目)什么是事件循环
金山软件
AI 全栈开发工程师了解事件循环吗?
智元机器人
前端开发工程师浏览器和JS的异步策略是什么?事件循环是如何工作的?
虾皮
前端开发工程师事件循环,宏任务微任务有哪些
雷鸣比特
前端开发工程师讲一讲事件循环机制
展开剩余 1 篇面经
阿里巴巴
前端开发工程师JS是单线程的,如果要处理比较重的后台任务或者网络请求,它是怎么弄的?

