浏览器事件循环详解(基于最新 W3C 概念)

82 阅读8分钟

本文为个人学习笔记,用于梳理浏览器进程、渲染进程及事件循环相关知识,便于复习和总结。

浏览器进程与渲染进程概览

现代浏览器采用多进程架构比如谷歌浏览器的Chrome Web,将不同功能模块分离运行,以提升稳定性、安全性和性能。常见的进程包括:

  • 浏览器进程,负责浏览器界面显示、标签页管理、文件访问、权限控制以及与其他进程的调度协调。
  • 渲染进程,是每个标签页的核心,负责解析 HTML、CSS、执行 JavaScript、构建渲染树并最终绘制页面。
  • 网络进程,专门处理网络请求、资源加载、缓存及 Cookie 管理。
  • GPU 进程,承担页面的图形绘制与硬件加速任务,如合成图层、渲染视频或 Canvas。
  • 插件进程,用于运行 PDF Viewer 等插件,防止其崩溃影响浏览器整体。各进程之间通过进程间通信(IPC)进行协作,例如渲染进程向浏览器进程请求资源、网络进程返回数据给渲染进程。通过这种多进程隔离设计,浏览器实现了更高的稳定性与安全性,即使某个标签页或插件崩溃,也不会导致整个浏览器崩溃。
  • 渲染进程,负责:解析 HTML、CSS 和 JavaScript构建 DOM 树和 CSSOM 树生成渲染树(Render Tree)布局(Layout / Reflow)绘制(Painting)响应用户交互和执行 JavaScript

我们常见的浏览器架构包括 浏览器进程网络进程渲染进程 。其中,每一个页面(Tab)通常对应一个独立的 渲染进程。在渲染进程内部,还包含多个线程,例如合成线程光栅化线程IO 线程等。而其中最核心、最关键的线程就是 渲染主线程(Main Thread),我们熟知的 事件循环(Event Loop) 机制,正是运行在这个渲染主线程中。值得注意的是,渲染主线程是单线程的 —— 也就是说,它在同一时刻只能执行一个任务。


渲染主线程为什么是单线程

在浏览器中,渲染主线程(Main Thread) 是最核心的执行环境,负责处理包括 JavaScript 执行 等一系列关键任务。
由于 JavaScript 可以直接操作 DOM 和 CSSOM,而这两者又与页面渲染紧密相关,如果允许多个线程同时修改这些结构,就可能出现 数据竞争(Data Race)状态不一致(Inconsistency) 的问题,最终导致页面渲染混乱甚至崩溃。

因此,浏览器在设计上采用了 单线程模型 来执行 JavaScript,从根本上保证了 DOM 操作的安全性和渲染结果的一致性。

简单来说——因为 JavaScript 的执行可能会修改 DOM,而 DOM 必须是安全且一致的,所以 JavaScript 必须在单线程中运行。

然而,单线程模型也带来了一个问题:主线程在执行任务时无法同时处理耗时操作。例如,当执行到网络请求、定时器(setTimeout)或文件读取等任务时,如果主线程选择“等待”结果返回,就会导致整个页面被阻塞,无法响应用户操作,出现“卡死”的情况。为了解决这一问题,浏览器引入了异步任务机制,让这些需要等待的操作能够在主线程之外完成,等结果准备好后再通知主线程继续执行。


什么是异步任务

所谓 异步任务(Asynchronous Task) ,指的是那些不会阻塞主线程、而是在后台独立执行,完成后再将结果交回主线程处理的任务

在浏览器中,除了负责执行 JavaScript 的 渲染主线程(Main Thread) 外,还存在多个用于处理不同类型任务的 辅助线程,例如:

  • 计时线程:负责 setTimeoutsetInterval 等定时任务的计时;
  • 🌐 网络线程:负责发起并监听网络请求,当响应返回后触发回调;
  • 🖱 事件线程:监听用户的点击、输入、滚动等交互事件;

当主线程执行 JavaScript 时,如果遇到这些需要等待的操作(如定时器、网络请求、事件回调等),它不会阻塞等待结果,而是将任务交给对应的辅助线程去处理
辅助线程在后台完成任务后,会将回调函数封装成一个任务对象,并放入消息队列(Message Queue) 中等待主线程空闲时再执行。

这种机制使得主线程能够持续执行其他任务,而不会因为某个耗时操作而被“卡住”。
换句话说,异步任务让单线程的 JavaScript 拥有了“并行”的效果 —— 既保证了渲染安全,又提升了执行效率。

为了让这些来自不同来源的任务(如计时器回调、网络响应、用户交互等)能够有序地被调度执行,浏览器引入了两个核心机制:
👉 消息队列(Message Queue)事件循环(Event Loop)


消息队列与事件循环

浏览器中存在大量异步操作,例如用户点击事件、定时器回调、Promise 回调以及网络请求返回的数据。如果没有机制管理这些异步任务,就可能导致执行顺序混乱或阻塞页面渲染。消息队列(Task Queue / 任务队列)事件循环(Event Loop) 是浏览器调度异步任务的关键机制。

  • 消息队列(Message Queue / Task Queue) :用来存放异步任务的回调函数。(消息队列是所有微任务队列、延时队列、交互队列...的统称)。
  • 事件循环(Event Loop) :不断检查消息队列,并将任务有序分配给渲染主线程执行。

这种 “单线程 + 消息队列 + 事件循环” 的设计,既保证了页面状态安全与一致,又让渲染任务与异步操作有序协同。JavaScript 主线程按任务队列顺序逐一执行,渲染进程在空闲时绘制页面,从而实现流畅的页面渲染和高效的异步处理。

常见消息队列及优先级

队列 / 类型举例优先级
微任务队列Promise.thenqueueMicrotaskMutationObserver最高
脚本执行队列<script> 标签同步代码
动画队列(帧队列)requestAnimationFrame中高
用户交互队列鼠标点击、键盘事件回调
计时器 / 延时队列setTimeoutsetInterval中低
空闲队列requestIdleCallback

根据 W3C 最新标准,任务不再区分“宏任务/微任务”,而是根据类型分配到不同队列。队列内部的任务按顺序执行,不同队列按优先级调度。微任务队列优先级最高。


事件循环执行流程

渲染主线程在执行 全局JS 时:

渲染主线程会直接执行同步代码;遇到异步代码(如 Promise.thensetTimeout 等)时,不会立即执行,而是 根据类型将回调注册到相应的队列。 例如,当渲染主线程遇到 setTimeout 时,会通知计时器线程开始计时。计时结束后,计时器线程会将回调包装成任务加入消息队列,等待渲染主线程空闲时执行。遇到Promise.then会将回调加到微任务队列里面 。

渲染主线程在完成当前轮次的所有任务后,会根据各队列的优先级顺序,依次从消息队列中取出任务执行。在执行每个任务期间产生的新任务,也会按照相同的流程注册到对应队列并等待调度。

整个过程不断重复,保证异步任务、页面渲染和用户交互在单线程环境下有序进行,这一持续调度机制就是 事件循环


案例解析

setTimeout(() => {
  console.log("a");

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

new Promise((resolve) => {
  console.log("b");
  resolve();
}).then(() => {
  console.log("then1");
});

执行流程:

  • 首先要执行全局js,只有当全局js执行完成以后才会去执行其他队列的任务。
  • 执行到setTimeout,通知计时器进行计时,继续执行剩余代码,等计时结束以后把setTimeout的回调函数放到延时队列里面。
  • 接着执行new Promise(这里注意new Promise的回调是同步任务,直接执行,.then是微任务),打印出来b,继续执行,.then()是一个微任务,放到微任务队列里面。
  • 全局js执行完成以后,根据队列顺序执行剩余任务
  • 延时队列优先级低于微任务队列,所以先执行.then回调打印then1
  • 接着执行延时队列setTimeout回调,首先打印出来a,接着执行new Promise,打印.then

内部的 new Promise(...).then(...) 可以等同于 Promise.resolve().then(...)

最终打印顺序:

b
then1
a
then2

💡 核心总结

  • 事件循环(Event Loop) :渲染主线程不断检查各类消息队列,将任务按类型分配执行;队列内部按顺序执行,队列之间按优先级调度,从而实现异步任务调度与页面渲染的有序进行。

  • 这是我第一次整理写作,如果有表述不准确的地方,欢迎大家指正与讨论,一起优化完善。