浏览器中的 Event Loop:如何协调 JS、DOM 与渲染的「三角关系」?

84 阅读4分钟

深入理解 Event Loop:JavaScript 异步执行的核心机制

本文将从操作系统进程与线程的基本概念出发,结合浏览器多进程架构和 JavaScript 单线程模型,深入剖析 Event Loop 的运行机制,并解释宏任务、微任务之间的关系,以及它们如何影响页面渲染和性能优化。

一、进程与线程

进程(Process)

  • CPU 分配资源的最小单位
  • 每个进程拥有独立的内存空间和进程 ID(PID),一个进程崩溃不会影响其他进程。
  • 程序运行是以进程为单位进行的。
  • 主进程负责管理子进程,实现并发与并行操作。
  • 进程之间通信(IPC)开销较大,尤其是无关联进程之间的通信;父子进程之间的通信效率更高。

线程(Thread)

  • CPU 调度的最小单位
  • 线程是进程内部的执行单元,多个线程共享同一个进程的资源。
  • 在浏览器中,每个进程可以包含多个线程,例如主线程、定时器线程、网络线程等。

二、浏览器的多进程架构

现代浏览器采用 多进程架构 来提高安全性和稳定性:

  • 启动时会有一个主进程,负责协调和管理子进程。

  • 每个标签页(Tab)对应一个渲染进程,彼此互不干扰。

  • 渲染进程中包含主线程(JS 执行线程)、渲染线程、事件触发线程

    主线程负责解析 HTML 构建 DOM 树、解析 CSS 构建 CSSOM 树,合并生成渲染树、布局计算(Layout)、图层合并(Paint)、V8 引擎执行 JavaScript。

    渲染线程负责将渲染树(Render Tree)转换为屏幕上的像素(栅格化),并处理重绘(Repaint)和回流(Reflow)。

    **主线程与渲染线程是互斥的:**同步 JS 执行期间,页面不会重新渲染。JS 执行完毕后,才会触发渲染流程(布局、绘制等)。即使渲染任务已经准备好,也必须等待 JS 主线程空闲后才能执行。

    OIP.png

三、Event Loop:JavaScript 异步执行的引擎

由于 JavaScript 是单线程的,为了不让主线程被长时间阻塞,JavaScript 引入了 异步非阻塞模型,其背后的核心机制就是 Event Loop

执行栈(Call Stack)

  • 所有同步代码都在执行栈中按顺序执行。
  • 遇到函数调用就将其压入栈顶,函数返回则弹出。

宿主环境(Host Environment)

  • 浏览器提供了一些额外的功能,如 setTimeoutsetIntervalfetchaddEventListener 等。
  • 这些功能由不同的线程或系统调用完成(如定时器线程、网络线程)。
  • 当这些异步操作完成后,它们会将回调放入相应的任务队列中。

宏任务队列(Macro Task Queue),每次 Event Loop 循环处理一个宏任务。

​ 宏任务包括:setTimeoutsetIntervalsetImmediate(Node.js 中)、I/O 操作、UI 渲染(某些浏览器中)

微任务队列(Micro Task Queue),微任务优先级高于宏任务,在当前宏任务结束后立即清空所有微任务。

​ 微任务包括:Promise.then, .catch, .finallyqueueMicrotaskMutationObserver

四、Event Loop 的执行流程详解

完整的 Event Loop 执行流程如下:

  1. 宏任务进入执行栈,执行同步任务(进入执行栈)
  2. 遇到异步任务setTimeout、fetch
    • 交给对应的宿主线程处理(如定时器线程),处理完成后,将回调放入对应的任务队列
  3. 同步任务执行完毕后检查是否有微任务,如果有,依次执行直到微任务队列清空
  4. 渲染页面
  5. 执行一个宏任务
  6. 重复上述流程

16cb1d70e5120bea~tplv-t2oaga2asx-zoom-in-crop-mark_1512_0_0_0.png

五、关于 setTimeout 的一些细节

console.log("Start");

setTimeout(() => {
    console.log("Timeout 1");
}, 0);

Promise.resolve().then(() => {
    console.log("Promise 1");
});

setTimeout(() => {
    console.log("Timeout 2");
}, 0);

console.log("End");

输出结果为:

Start
End
Promise 1
Timeout 1
Timeout 2

分析:

  • "Start""End" 是同步任务,直接打印。
  • Promise.then 是微任务,进入微任务队列。
  • 两个 setTimeout 是宏任务,进入宏任务队列。
  • 同步任务执行完后,先执行微任务(Promise 1),然后才轮到宏任务。

七、Event Loop 实战技巧与优化建议

  1. 使用 Promise 替代 setTimeout(..., 0) 来实现异步延迟,因为 Promise 属于微任务,优先级更高,执行更快。

    // 不推荐
    setTimeout(() => { /* ... */ }, 0);
    
    // 推荐
    Promise.resolve().then(() => { /* ... */ });
    
  2. 控制宏任务数量,避免阻塞主线程

    避免大量使用 setTimeoutsetInterval。可以使用 requestIdleCallback(浏览器环境下)来利用空闲时间执行低优先级任务。

  3. 使用 Web Worker 处理耗时任务

    将计算密集型任务移出主线程,避免阻塞渲染和交互。

八、总结

概念描述
进程CPU 分配资源的最小单位,具有独立内存空间
线程CPU 调度的最小单位,多个线程共享进程资源
主线程执行 JavaScript 同步代码,负责页面渲染与 DOM 操作
Event Loop协调宏任务与微任务的执行顺序
宏任务setTimeoutsetInterval,每次循环执行一个
微任务Promise.then,优先级更高,同步任务执行完立刻执行