在浏览器环境中,JavaScript 事件循环(Event Loop)和渲染事件 的执行过程是 Web 性能优化和流畅交互的重要基础。以下是详细的执行流程,包括 同步任务、微任务、宏任务、渲染机制 等所有可能的节点。
📌 1. 事件循环(Event Loop)整体执行流程
浏览器的 JavaScript 运行环境基于 事件循环(Event Loop) ,其执行逻辑如下:
-
执行同步代码(Script)
- 代码最先进入 调用栈(Call Stack) ,按照顺序执行 同步任务(Synchronous Tasks)。
-
执行微任务(Microtasks)
-
运行所有 微任务(Microtasks) ,包括:
Promise.then/catch/finally
MutationObserver
queueMicrotask()
-
-
执行宏任务(Macrotasks)
-
运行 一个 宏任务(Macrotask),包括:
setTimeout
setInterval
setImmediate
(仅 Node.js)requestAnimationFrame
I/O 操作
UI 渲染
MessageChannel
-
-
渲染更新(仅浏览器)
- 如果 JavaScript 执行结束,浏览器会检查是否需要更新 UI,并执行 下一帧的渲染(Frame Rendering) 。
-
重复以上过程
- 事件循环(Event Loop)继续重复上述流程。
📌 2. 浏览器的执行步骤细节
浏览器的事件循环不仅仅处理 JavaScript 代码,还涉及 渲染、UI 更新,主要执行顺序如下:
🔹 1. 解析 HTML & 构建 DOM
- 浏览器解析 HTML ,将其转换为 DOM 树(DOM Tree) 。
🔹 2. 解析 CSS & 构建 CSSOM
- CSS 解析器 解析
link
或style
内嵌 CSS,构建 CSSOM(CSS 对象模型) 。
🔹 3. 运行 JavaScript
-
执行 JavaScript 代码:
- 运行 同步代码(同步任务) ,如变量赋值、函数调用等。
- 当遇到 异步任务 (如
setTimeout
或Promise
),它们会被放入相应的 任务队列(Task Queue) ,等待执行。
🔹 4. 处理微任务(Microtasks)
-
微任务优先级高于宏任务 ,当同步代码执行完成后,立即执行所有 微任务:
- 例如
Promise.then()
、MutationObserver
、queueMicrotask()
。
- 例如
🔹 5. 处理宏任务(Macrotasks)
-
事件循环每次从宏任务队列取出 一个 任务执行,包括:
setTimeout
setInterval
requestAnimationFrame
I/O 任务
MessageChannel
🔹 6. 计算布局(Recalculate Style & Layout)
-
如果 DOM 或 CSS 发生变化 ,浏览器会 重新计算元素的样式和布局:
- Recalculate Style(重新计算样式)
- Layout(布局计算,确定元素的位置和大小)
🔹 7. 处理合成(Paint & Composite)
- Paint(绘制) :将像素绘制到屏幕上。
- Composite(合成) :合成多个图层,显示最终画面。
📌 3. 详细的事件执行顺序(实例解析)
我们来看一个具体的案例:
javascript
console.log("同步代码开始"); // 1️⃣
setTimeout(() => {
console.log("setTimeout 宏任务"); // 6️⃣
}, 0);
Promise.resolve().then(() => {
console.log("Promise 微任务"); // 3️⃣
});
console.log("同步代码结束"); // 2️⃣
📝 执行顺序解析
步骤 | 任务类型 | 代码 | 说明 |
---|---|---|---|
1️⃣ | 同步任务 | console.log("同步代码开始") |
直接执行 |
2️⃣ | 同步任务 | console.log("同步代码结束") |
直接执行 |
3️⃣ | 微任务 | Promise.then() |
微任务放入微任务队列,立即执行 |
4️⃣ | 检查是否有剩余微任务 | 微任务队列为空,继续 | |
5️⃣ | 宏任务 | setTimeout |
执行定时器回调 |
6️⃣ | 日志输出 | "setTimeout 宏任务" |
最终 控制台输出顺序:
javascript
同步代码开始
同步代码结束
Promise 微任务
setTimeout 宏任务
📌 4. requestAnimationFrame(帧渲染机制)
requestAnimationFrame()
是专门用于优化动画的 宏任务,它的特点是:
- 在浏览器下一帧渲染前执行(通常 16.67ms 一次)。
- 比
setTimeout(fn, 16)
更精准,且不会阻塞 UI 渲染。
示例:
scss
function renderFrame() {
console.log("下一帧渲染");
requestAnimationFrame(renderFrame);
}
requestAnimationFrame(renderFrame);
执行时机:
- 浏览器刷新时触发(即下一帧到来前)。
- 如果页面切换到后台,动画会暂停,节省 CPU 资源。
📌 5. setTimeout
vs requestAnimationFrame
机制 | 适用场景 | 运行频率 | 是否阻塞渲染 |
---|---|---|---|
setTimeout(fn, 16) |
粗略的定时器 | 约 16ms 一次 | 可能阻塞渲染 |
requestAnimationFrame(fn) |
平滑动画 | 下一帧 渲染前 | 不阻塞,推荐 |
🚀 建议: 使用 requestAnimationFrame()
来实现动画,而不是 setTimeout(fn, 16)
,能提供 更流畅的动画效果!
📌 6. 总结
🔹 事件循环的完整顺序
- 执行 同步任务(script)。
- 执行 所有微任务(Promise、MutationObserver)。
- 渲染(如果需要)。
- 执行 一个 宏任务(setTimeout, setInterval)。
- 重复循环。
🔹 渲染过程
- JS 执行完毕
- 计算样式(Recalculate Style)
- 计算布局(Layout)
- 绘制(Paint)
- 合成(Composite)
- 显示画面
📢 优化建议
- 使用
requestAnimationFrame()
优化动画 - 减少 DOM 操作,避免 Repaint & Reflow
- 合理使用
setTimeout
,避免阻塞渲染
这样,你就完整掌握了浏览器 JS 事件循环 和 渲染机制 的详细流程了!🚀