本文为个人学习笔记,用于梳理浏览器进程、渲染进程及事件循环相关知识,便于复习和总结。
浏览器进程与渲染进程概览
现代浏览器采用多进程架构比如谷歌浏览器的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) 外,还存在多个用于处理不同类型任务的 辅助线程,例如:
- ⏱ 计时线程:负责
setTimeout、setInterval等定时任务的计时; - 🌐 网络线程:负责发起并监听网络请求,当响应返回后触发回调;
- 🖱 事件线程:监听用户的点击、输入、滚动等交互事件;
当主线程执行 JavaScript 时,如果遇到这些需要等待的操作(如定时器、网络请求、事件回调等),它不会阻塞等待结果,而是将任务交给对应的辅助线程去处理。
辅助线程在后台完成任务后,会将回调函数封装成一个任务对象,并放入消息队列(Message Queue) 中等待主线程空闲时再执行。
这种机制使得主线程能够持续执行其他任务,而不会因为某个耗时操作而被“卡住”。
换句话说,异步任务让单线程的 JavaScript 拥有了“并行”的效果 —— 既保证了渲染安全,又提升了执行效率。
为了让这些来自不同来源的任务(如计时器回调、网络响应、用户交互等)能够有序地被调度执行,浏览器引入了两个核心机制:
👉 消息队列(Message Queue) 和 事件循环(Event Loop) 。
消息队列与事件循环
浏览器中存在大量异步操作,例如用户点击事件、定时器回调、Promise 回调以及网络请求返回的数据。如果没有机制管理这些异步任务,就可能导致执行顺序混乱或阻塞页面渲染。消息队列(Task Queue / 任务队列) 和 事件循环(Event Loop) 是浏览器调度异步任务的关键机制。
- 消息队列(Message Queue / Task Queue) :用来存放异步任务的回调函数。(消息队列是所有微任务队列、延时队列、交互队列...的统称)。
- 事件循环(Event Loop) :不断检查消息队列,并将任务有序分配给渲染主线程执行。
这种 “单线程 + 消息队列 + 事件循环” 的设计,既保证了页面状态安全与一致,又让渲染任务与异步操作有序协同。JavaScript 主线程按任务队列顺序逐一执行,渲染进程在空闲时绘制页面,从而实现流畅的页面渲染和高效的异步处理。
常见消息队列及优先级
| 队列 / 类型 | 举例 | 优先级 |
|---|---|---|
| 微任务队列 | Promise.then、queueMicrotask、MutationObserver | 最高 |
| 脚本执行队列 | <script> 标签同步代码 | 高 |
| 动画队列(帧队列) | requestAnimationFrame | 中高 |
| 用户交互队列 | 鼠标点击、键盘事件回调 | 中 |
| 计时器 / 延时队列 | setTimeout、setInterval | 中低 |
| 空闲队列 | requestIdleCallback | 低 |
根据 W3C 最新标准,任务不再区分“宏任务/微任务”,而是根据类型分配到不同队列。队列内部的任务按顺序执行,不同队列按优先级调度。微任务队列优先级最高。
事件循环执行流程
渲染主线程在执行 全局JS 时:
渲染主线程会直接执行同步代码;遇到异步代码(如 Promise.then、setTimeout 等)时,不会立即执行,而是 根据类型将回调注册到相应的队列。 例如,当渲染主线程遇到 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) :渲染主线程不断检查各类消息队列,将任务按类型分配执行;队列内部按顺序执行,队列之间按优先级调度,从而实现异步任务调度与页面渲染的有序进行。
-
这是我第一次整理写作,如果有表述不准确的地方,欢迎大家指正与讨论,一起优化完善。