主题
08 · 浏览器底层:事件循环与渲染流水线
目标:讲清 宏任务 / 微任务 /
requestAnimationFrame的执行顺序;建立「一帧里发生了什么」的直觉;用 2~3 个小 demo 自证,并能口述给面试官。
1. 背景:会写业务,但「卡一下」说不清
改 Journal 长列表或动画时,常听到「放 nextTick」「用 rAF」。若只有背诵题感,很难判断:setTimeout(0) 和 Promise.then 谁先、连续改 DOM 会不会闪、为什么滚动时 JS 算太久会掉帧。
这篇不替代规范原文,目标是可验证的顺序模型 + 渲染流水线粗线。
2. 核心概念
2.1 事件循环(Event Loop)
浏览器主线程大致循环:
text
取一个宏任务(macrotask)→ 执行 → 清空微任务队列(microtask)→ (可能)渲染 → 下一个宏任务1
| 类型 | 常见来源 |
|---|---|
| 宏任务 | script、setTimeout/setInterval、I/O、UI 事件(click) |
| 微任务 | Promise.then、queueMicrotask、MutationObserver |
| rAF 回调 | 在下一次 repaint 之前,通常在本轮微任务清空之后 |
记忆:一次宏任务结束后,会把当前微任务队列全部跑完,再考虑渲染;所以 Promise.then 往往比 setTimeout(0) 先执行。
2.2 一帧里的渲染流水线(简化)
text
JS 执行 → Style → Layout(Reflow)→ Paint → Composite1
- Layout:几何变化(读
offsetWidth、改width)贵 - Composite:仅
transform/opacity往往更省 - rAF:在浏览器打算绘制前给你一次「改视觉状态」的钩子,适合跟屏幕刷新对齐的动画
与 Vue:nextTick 用的是微任务(或 MutationObserver),在 DOM 更新后、浏览器绘制前把回调排进队——所以能在「数据变了、DOM 还没画出来」时读到新节点。
3. 最小实践:三个顺序 demo
在浏览器控制台或任意 HTML 里跑。
Demo 1 — 宏 vs 微
javascript
console.log('1 sync')
setTimeout(() => console.log('2 timeout'), 0)
Promise.resolve().then(() => console.log('3 micro'))
console.log('4 sync')
// 1 → 4 → 3 → 21
2
3
4
5
2
3
4
5
Demo 2 — rAF 与 microtask
javascript
Promise.resolve().then(() => console.log('micro'))
requestAnimationFrame(() => console.log('raf'))
console.log('sync')
// sync → micro → raf(同一轮内 micro 先于 rAF)1
2
3
4
2
3
4
Demo 3 — 布局抖动(读写交错)
javascript
const box = document.querySelector('#box')
box.style.width = '100px'
console.log(box.offsetWidth) // 强制 layout
box.style.width = '200px'
// 连续读写交错会触发多次 layout;批量改 class 或 rAF 里统一读1
2
3
4
5
2
3
4
5
Journal 对照:列表虚拟滚动若在 scroll 事件里做重计算,会占满宏任务,挤掉绘制——应 throttle / rAF / Worker(见系列 13)。
4. 踩坑与取舍
| 场景 | 坑 | 建议 |
|---|---|---|
| 动画 | 用 setInterval 改 left | 优先 CSS transform + rAF |
| 数据更新 | 以为 setTimeout(0) 比 then 早 | 要「DOM 更新后」用 Vue nextTick,不是 setTimeout |
| 长任务 | 一次 JS > 50ms | 拆片、scheduler.postTask、Worker |
| 面试 | 背「先微后宏」但说不清 rAF | 用 Demo 1/2 现场跑 |
不必深挖:各浏览器调度细节、Prioritized Task Scheduling 优先级——除非岗位明确考 Chromium 内核。
5. 面试口述模板(30 秒版)
一次事件循环先执行当前宏任务,然后清空所有微任务;如果需要渲染,会走 style/layout/paint/composite。
requestAnimationFrame回调安排在下一帧绘制前,适合做视觉更新。Promise.then是微任务,所以通常比setTimeout更早。Vue 的nextTick让我在 DOM patch 完成后、浏览器绘制前跑回调,用来读布局或聚焦输入框。
6. 验收清单
- [ ] 独立跑出 Demo 1、2,顺序与预期一致
- [ ] 能画「宏任务 → 微任务 → 渲染」简图
- [ ] 说清 Layout 与 Composite 各对应什么 CSS 改动
- [ ] 用上面模板讲一遍,不卡壳
