浏览器环境下的 JS Event Loop
解释浏览器端 JavaScript Event Loop 中 task、microtask checkpoint、run-to-completion 与 rendering opportunity 的关系,并澄清同步/异步不是任务队列类型。
[!info] related notes
- 所属:JavaScript 异步与并发 MOC
- 前置:调用栈与执行上下文 · 同步与异步
- 相关:Promise · 定时器与事件监听
- 对比:浏览器与 Node.js 事件循环的区别 · Node.js 事件循环阶段
浏览器环境下的 JS Event Loop
一句话定义
浏览器 Event Loop 协调主线程上的 JavaScript 与页面更新。一个实用且更准确的模型是:
选择一个 task
↓
执行其中的 JavaScript(run-to-completion)
↓
microtask checkpoint:持续清空 microtask queue
↓
浏览器可能获得 rendering opportunity
↓
选择下一个 task
两套不能混用的分类
同步 / 异步描述工作如何完成
setTimeout(fn, 1000) 这个函数调用本身同步执行,它只向宿主注册 timer;达到时间阈值后,fn 才成为可调度的 task。
因此不要记成:
异步代码 → 立刻进入任务队列
更准确的是:
注册异步操作
→ 浏览器 / 操作系统等待或处理
→ 条件满足
→ 对应 JavaScript 回调被安排为 task 或 microtask
task / microtask 描述回调如何被调度
同步代码不是第三种队列。当前 task 被选中后,其中可立即执行的 JavaScript 进入调用栈,并持续运行到这段 JavaScript 返回。
Task 是什么
task 是浏览器事件循环的一次主要工作单元。很多教程称它为 macrotask,但正式解释时优先说 task。
常见 task 来源:
- 初始
<script>; setTimeout/setInterval回调;- 用户输入和 DOM 事件回调;
- 消息及部分网络 / I/O 回调。
规范中存在不同 task source,并不是只有一个抽象的“宏任务队列”。普通面试无需背 task source 调度细节,但不要把所有任务想成一个全局 FIFO。
Microtask checkpoint
常见 microtask 来源:
Promise.then / catch / finally;queueMicrotask();MutationObserver。
当前 JavaScript task 结束且调用栈为空后,浏览器会执行 microtask checkpoint。执行 microtask 时新加入的 microtask 也会在进入下一个 task 前继续执行,所以递归 microtask 可能长期推迟渲染和用户事件。
最小例子
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')
当前 script task:A → 注册 timer → 注册 Promise reaction → D
microtask checkpoint:C
后续 timer task:B
输出:
A
D
C
B
这里不是“Promise 天生比 timer 优先”,而是 Promise reaction 被安排进当前 task 结束后的 microtask checkpoint,timer callback 则要等后续 task。
Rendering opportunity
浏览器还要处理 style、layout、paint、composite 和用户输入。因此每轮 task 与 microtask checkpoint 后,浏览器可能获得一次渲染机会。
“可能”很重要:浏览器不保证每个 task 后都立即绘制。requestAnimationFrame 与渲染时机绑定,也不应简单等同于 setTimeout。
为什么“同步 → 微任务 → 宏任务”不是最终模型
这个口诀只能帮助解最简单输出题,容易制造三个误解:
- 误以为存在“同步任务队列”;
- 误以为浏览器只有一个宏任务队列;
- 忽略 rendering opportunity 和 microtask starvation。
最终应记:
同步 / 异步描述工作如何完成;task / microtask 描述回调回来后如何调度。一个 task 中的 JavaScript run-to-completion,随后执行 microtask checkpoint,浏览器才可能渲染并选择下一个 task。
常见误区
JavaScript 单线程 = 浏览器单线程
错误。主线程上的 JavaScript 通常 run-to-completion,但浏览器整体还会使用网络线程、渲染线程、Worker 和操作系统能力。
setTimeout(fn, 0) = 立即执行
错误。0 只表示满足最小延迟后可被调度,还要等待当前 JavaScript、microtasks 和浏览器调度。
Promise 是整体异步执行
要拆开看:new Promise(executor) 的 executor 同步执行;.then() reaction 在 Promise settle 后作为 microtask 运行。
microtask 是“更高优先级的宏任务”
不准确。microtask 有独立的 checkpoint 规则,并在进入下一个 task 前持续清空。
面试回答
浏览器 Event Loop 主要协调 task、microtask checkpoint 和页面渲染。Event Loop 选择一个 task 后,其中的 JavaScript 在调用栈上 run-to-completion;栈清空后执行 microtask checkpoint,把当前排队的 Promise reaction、queueMicrotask 等持续清空,然后浏览器可能获得一次渲染机会,再选择下一个 task。同步/异步与 task/microtask 不是同一个分类维度,所以我不会只用“同步、微任务、宏任务”这个口诀解释完整模型。