深入 Event Loop(进阶篇)—— 浏览器多进程与渲染机制

88 阅读3分钟

前言

JavaScript 单线程设计的背后,是浏览器强大的多进程、多线程架构支持。我们来看一下浏览器如何配合 JavaScript 实现高效的事件循环和页面渲染。

一、浏览器的多进程架构

现代浏览器(如 Chrome)采用多进程 + 多线程架构:

  • 主进程:负责管理标签页、插件等。

  • 每个标签页是一个独立的渲染进程

    • 防止页面崩溃影响其他页面。
    • 提升安全性和稳定性。

渲染进程的主要职责:

  1. 解析 HTML 构建 DOM 树。
  2. 解析 CSS 构建 CSSOM 树。
  3. 构建渲染树(Render Tree)。
  4. 布局(Layout)计算元素位置。
  5. 绘制(Paint)到屏幕。
  6. 合成(Composite)图层提交给 GPU。

这些步骤都由主线程完成,而 JS 也是在这条主线程上运行的。

二、JS 与渲染互斥

由于 JS 和页面渲染都在同一线程中执行,因此它们是互斥的:

  • 当 JS 正在执行时,页面无法渲染。
  • 当页面正在渲染时,JS 无法执行。

这也是为何 长时间的同步任务会导致页面“卡死” 的原因。

三、事件队列与异步线程

虽然 JS 是单线程的,但浏览器为一些耗时任务提供了专属线程来处理:

异步任务类型是否有专属线程说明
setTimeout / setInterval✅ 有定时器线程到时间后将回调放入宏任务队列
fetch / XHR✅ 有网络线程请求完成后将回调放入宏任务队列
addEventListener❌ 没有直接注册到事件队列中

四、事件循环机制详解

1. 宏任务(Macrotask)

  • 每次事件循环处理一个宏任务。

  • 宏任务包括:

    • 整个 <script> 脚本
    • setTimeout
    • setInterval
    • 用户交互事件等

2. 微任务(Microtask)

  • 在当前宏任务结束后立即执行。

  • 包括:

    • Promise.then/catch/finally
    • MutationObserver
    • queueMicrotask
    • Node.js 中的 process.nextTick

3. 执行顺序

  1. 执行宏任务(如 <script> 中的同步代码)。
  2. 清空微任务队列。
  3. 触发页面渲染。
  4. 进入下一轮事件循环。

📌 注意:页面渲染发生在宏任务结束、微任务清空之后。

4. 示例分析

console.log("Start");
process.nextTick(() => {
    console.log("Process next tick");
});
Promise.resolve().then(() => {
    console.log("Promise Resolved");
});
setTimeout(() => {
    console.log("setTimeout");
}, 0);
console.log("End");

// 输出顺序:
// Start -> End -> Process next tick -> Promise Resolved -> setTimeout

五、常见问题解析

1. 为什么 setTimeout(0) 不一定准时?

  • 因为 setTimeout 是宏任务,必须等到当前宏任务和所有微任务执行完毕才可能被执行。
  • 如果主线程被阻塞,即使时间到了也无法执行。

2. 如何优化页面性能?

  • 尽量减少同步任务耗时。
  • 使用微任务进行 DOM 更新后的处理。
  • 使用 Web Worker 处理复杂计算,避免阻塞主线程。

六、结语

理解 Event Loop 不仅能帮助我们写出更高效的 JavaScript 代码,还能让我们更好地理解浏览器的工作机制。掌握宏任务、微任务、渲染时机之间的关系,是前端开发进阶的关键一步!