一文搞清楚浏览器js事件循环和渲染事件

193 阅读4分钟

在浏览器环境中,JavaScript 事件循环(Event Loop)渲染事件的执行过程是 Web 性能优化和流畅交互的重要基础。以下是详细的执行流程,包括 同步任务、微任务、宏任务、渲染机制 等所有可能的节点。


📌 1. 事件循环(Event Loop)整体执行流程

浏览器的 JavaScript 运行环境基于 事件循环(Event Loop) ,其执行逻辑如下:

  1. 执行同步代码(Script)

    • 代码最先进入 调用栈(Call Stack) ,按照顺序执行 同步任务(Synchronous Tasks)。
  2. 执行微任务(Microtasks)

    • 运行所有 微任务(Microtasks) ,包括:

      • Promise.then/catch/finally
      • MutationObserver
      • queueMicrotask()
  3. 执行宏任务(Macrotasks)

    • 运行 一个 宏任务(Macrotask),包括:

      • setTimeout
      • setInterval
      • setImmediate(仅 Node.js)
      • requestAnimationFrame
      • I/O 操作
      • UI 渲染
      • MessageChannel
  4. 渲染更新(仅浏览器)

    • 如果 JavaScript 执行结束,浏览器会检查是否需要更新 UI,并执行 下一帧的渲染(Frame Rendering)
  5. 重复以上过程

    • 事件循环(Event Loop)继续重复上述流程。

📌 2. 浏览器的执行步骤细节

浏览器的事件循环不仅仅处理 JavaScript 代码,还涉及 渲染、UI 更新,主要执行顺序如下:

🔹 1. 解析 HTML & 构建 DOM

  • 浏览器解析 HTML,将其转换为 DOM 树(DOM Tree)

🔹 2. 解析 CSS & 构建 CSSOM

  • CSS 解析器 解析 linkstyle 内嵌 CSS,构建 CSSOM(CSS 对象模型)

🔹 3. 运行 JavaScript

  • 执行 JavaScript 代码

    • 运行 同步代码(同步任务) ,如变量赋值、函数调用等。
    • 当遇到 异步任务(如 setTimeoutPromise),它们会被放入相应的 任务队列(Task Queue) ,等待执行。

🔹 4. 处理微任务(Microtasks)

  • 微任务优先级高于宏任务,当同步代码执行完成后,立即执行所有 微任务

    • 例如 Promise.then()MutationObserverqueueMicrotask()

🔹 5. 处理宏任务(Macrotasks)

  • 事件循环每次从宏任务队列取出 一个 任务执行,包括:

    • setTimeout
    • setInterval
    • requestAnimationFrame
    • I/O 任务
    • MessageChannel

🔹 6. 计算布局(Recalculate Style & Layout)

  • 如果 DOM 或 CSS 发生变化,浏览器会 重新计算元素的样式和布局

    • Recalculate Style(重新计算样式)
    • Layout(布局计算,确定元素的位置和大小)

🔹 7. 处理合成(Paint & Composite)

  • Paint(绘制) :将像素绘制到屏幕上。
  • Composite(合成) :合成多个图层,显示最终画面。

📌 3. 详细的事件执行顺序(实例解析)

我们来看一个具体的案例:

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 宏任务"

最终 控制台输出顺序

同步代码开始
同步代码结束
Promise 微任务
setTimeout 宏任务

📌 4. requestAnimationFrame(帧渲染机制)

requestAnimationFrame() 是专门用于优化动画的 宏任务,它的特点是:

  1. 在浏览器下一帧渲染前执行(通常 16.67ms 一次)。
  2. setTimeout(fn, 16) 更精准,且不会阻塞 UI 渲染

示例:

function renderFrame() {
  console.log("下一帧渲染");
  requestAnimationFrame(renderFrame);
}

requestAnimationFrame(renderFrame);

执行时机

  • 浏览器刷新时触发(即下一帧到来前)。
  • 如果页面切换到后台,动画会暂停,节省 CPU 资源

📌 5. setTimeout vs requestAnimationFrame

机制适用场景运行频率是否阻塞渲染
setTimeout(fn, 16)粗略的定时器约 16ms 一次可能阻塞渲染
requestAnimationFrame(fn)平滑动画下一帧 渲染前不阻塞,推荐

🚀 建议: 使用 requestAnimationFrame() 来实现动画,而不是 setTimeout(fn, 16),能提供 更流畅的动画效果


📌 6. 总结

🔹 事件循环的完整顺序

  1. 执行 同步任务(script)。
  2. 执行 所有微任务(Promise、MutationObserver)。
  3. 渲染(如果需要)。
  4. 执行 一个 宏任务(setTimeout, setInterval)。
  5. 重复循环

🔹 渲染过程

  1. JS 执行完毕
  2. 计算样式(Recalculate Style)
  3. 计算布局(Layout)
  4. 绘制(Paint)
  5. 合成(Composite)
  6. 显示画面

📢 优化建议

  • 使用 requestAnimationFrame() 优化动画
  • 减少 DOM 操作,避免 Repaint & Reflow
  • 合理使用 setTimeout,避免阻塞渲染

这样,你就完整掌握了浏览器 JS 事件循环渲染机制 的详细流程了!🚀