第一部分:浏览器架构与线程模型
进程
进程是计算机中程序执行的基本单元,是操作系统分配资源和调度执行的单位。每个进程都有自己的内存空间、数据栈以及其他用于跟踪执行的辅助数据。
线程
线程是进程内部的一个执行流,是CPU调度的基本单位。一个进程可以包含一个或多个线程,这些线程共享进程的资源,如内存空间。
1.1 现代浏览器多进程架构
关键进程职责:
-
浏览器主进程:
-
管理用户界面(地址栏/书签)
-
协调子进程通信(IPC)、子进程管理(网络、插件、GPU、渲染进程)
-
处理文件访问权限
-
-
渲染进程(每个标签页独立):
-
浏览器会为每个标签页创建一个渲染进程(在某些情况下,例如开启“站点隔离”,还会为每个iframe创建单独的渲染进程)
-
HTML/CSS解析 → DOM树构建
-
JavaScript执行环境(V8引擎)
-
页面渲染管线管理
-
谷歌进程模型和站点隔离文档 站点隔离模式:
-
--site-per-process
:为每个站点(而不是每个标签页)启用单独的渲染进程。 -
--disable-site-isolation-trials
:禁用站点隔离试验。 -
其他模式和选项,用于控制站点隔离的行为。
-
-
-
网络进程:
-
HTTP/HTTPS请求处理
-
DNS预解析与缓存
-
QUIC协议实现
-
-
GPU 进程:
-
3D图形渲染(WebGL)
-
CSS动画硬件加速
-
视频解码
-
-
插件进程:
- 负责运行网页中使用的插件,例如Flash
1.2 线程
在浏览器的上下文中,以下线程尤为重要:
- 主线程:在渲染进程中,主线程负责解析HTML、CSS,执行JavaScript代码,以及绘制页面。它也被称为UI/渲染线程,因为它是处理用户界面相关操作的线程。
- GPU 线程:用于3D绘制和硬件加速。
- 网络线程:在浏览器进程和网络进程中,网络线程负责发起网络请求、处理响应等。
- IO 线程:用于处理磁盘读写操作。
运行代码的线程
在浏览器中,主线程(渲染进程中的主线程)负责执行JavaScript代码。当浏览器加载一个网页时,它会在渲染进程中解析HTML和CSS,构建DOM,然后主线程会执行JavaScript代码。由于JavaScript是单线程的,所以在同一个渲染进程中,JavaScript代码是在主线程上顺序执行的。
⚠️现代浏览器通过使用事件循环机制和Web Workers来允许JavaScript执行非阻塞操作。Web Workers运行在与主线程分离的背景线程中,允许执行长时间运行的计算而不会冻结用户界面。然而,所有的UI更新和大部分的Web API调用仍然需要在主线程上执行。
主线程(Main Thread)
// 伪代码展示主线程工作循环
while (true) {
Task task = taskQueue.pop();
switch (task.type) {
case DOM_UPDATE:
updateDOM(task.data);
break;
case JS_EXECUTION:
executeJavaScript(task.script);
break;
case EVENT_HANDLER:
dispatchEvent(task.event);
break;
}
checkMicrotasks(); // 执行所有微任务
}
通过事件循环(event loop/message loop)来进行循环处理,事件循环是一种处理异步事件和回调的机制,它确保了即使在单线程环境下,浏览器也能响应各种事件,同时保持用户界面的流畅性。
核心协作线程:
线程类型 | 职责 | 与主线程交互 |
---|---|---|
合成线程 | 图层分层管理 | 接收主线程的图层更新指令 |
光栅化线程 | 图层分块转位图 | 向GPU进程提交位图数据 |
Web Worker线程 | 执行后台计算 | 通过postMessage通信 |
Service Worker | 离线缓存管理 | 拦截网络请求 |
1.3 调用栈在渲染进程中的核心地位
调用栈(Call Stack) 是主线程的核心执行机制:
// 伪代码展示主线程工作循环
while (true) {
Task task = taskQueue.pop();
// 任务推入调用栈执行
callStack.push(task);
executeTask(task);
callStack.pop();
checkMicrotasks(); // 执行所有微任务
}
调用栈关键特性:
特性 | 说明 | 影响 |
---|---|---|
单线程执行 | 同一时间只能执行一个任务 | 避免多线程竞争问题 |
LIFO结构 | 后进先出 | 函数嵌套调用的基础 |
最大深度限制 | 约1000层(不同浏览器差异) | 防止无限递归导致崩溃 |
执行上下文管理 | 创建变量环境/词法环境 | 实现作用域链的基石 |
第二部分:事件循环机制
2.1 事件循环核心原理
2.2 微任务(Microtasks)深度解析
执行特性:
-
即时性:当前宏任务结束后立即执行
-
完全清空:必须执行完队列中所有微任务
-
可嵌套:微任务中产生的新微任务会立即执行
微任务来源:
// 1. Promise回调
Promise.resolve().then(() => {
console.log('Microtask from Promise');
});
// 2. MutationObserver
const observer = new MutationObserver(() => {
console.log('Microtask from DOM change');
});
observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true
});
// 3. queueMicrotask API
queueMicrotask(() => {
console.log('Explicit microtask');
});
2.3 宏任务(Macrotasks)层级架构
Chrome的多级队列系统:
队列优先级规则:
-
交互队列 > 网络队列 > 定时器队列
-
同类型队列先进先出
-
饥饿防护:每执行5个定时器任务,强制检查高优先级队列
2.4 事件触发线程工作原理
第三部分: 调用栈(Call Stack)基础原理
- 调用栈是一个后进先出(LIFO)的数据结构,用于存储在代码执行期间创建的执行上下文。
- 当JavaScript引擎执行函数时,它会创建一个新的执行上下文并将其推到调用栈的顶部。
- 当函数执行完成时,它的执行上下文会从调用栈中弹出,并且控制权返回到之前的上下文。
3.1 调用栈(Call Stack)基础原理
调用栈工作流程:
function a() { b(); }
function b() { c(); }
function c() { console.trace(); }
a(); // 启动调用链
调用栈与作用域链:
3.2 微任务执行时的调用栈行为
console.log('同步开始'); // 同步任务入栈
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => {
console.log('嵌套微任务'); // 微任务中的微任务
});
}); // 微任务入队
// 执行过程:
// 1. 同步代码执行(调用栈)
// 2. 调用栈空 → 清空微任务队列
// 3. 微任务1入栈执行
// 4. 嵌套微任务入队并立即执行
第四部分:浏览器 vs Node.js 事件循环
4.1 架构差异对比
特性 | 浏览器 | Node.js |
---|---|---|
实现基础 | 浏览器渲染引擎 | libuv库 |
线程模型 | 多进程协作 | 单进程+线程池 |
网络 I/O | 独立网络进程处理 | 线程池+epoll/kqueue |
渲染相关 | 集成渲染管线 | 无 |
优先级控制 | 多级队列系统 | 阶段轮询机制 |
4.2 Node.js事件循环阶段详解
各阶段职责:
-
timers:执行
setTimeout
和setInterval
回调 -
pending:处理系统级回调(如TCP错误)
-
poll:
- 检索新的I/O事件
- 执行I/O相关回调
- 计算阻塞时间
-
check:执行
setImmediate
回调 -
close:处理关闭事件(如
socket.on('close')
)
4.3 微任务执行差异
Node.js特有机制:
// 测试代码
setTimeout(() => console.log('timeout'));
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
/* 输出顺序:
nextTick → promise → timeout → immediate
*/
优先级规则:
-
process.nextTick
> 微任务 >setImmediate
-
Node.js v11+后微任务执行时机与浏览器对齐
4.4 跨环境案例分析
案例:文件读取顺序差异
// 浏览器环境
fetch('/data.json').then(handleData); // 微任务
setTimeout(renderUI, 0); // 宏任务
// Node.js环境
fs.readFile('data.json', (err, data) => { // I/O回调(poll阶段)
handleData(data);
setImmediate(() => console.log('After IO')); // check阶段
});
第五部分:深度案例解析
5.1 微任务递归陷阱
// 危险代码:微任务递归
function microtaskRecursion() {
Promise.resolve().then(() => {
console.log('Microtask executed');
microtaskRecursion(); // 递归调用
});
}
// 执行结果:阻塞主线程,页面卡死
解决方案:
// 安全模式:宏任务递归
function safeRecursion() {
console.log('Macrotask executed');
setTimeout(safeRecursion, 0); // 使用宏任务
}
5.2 跨文档通信机制
5.3 Service Worker生命周期
5.4 思考
可以考虑一下 第一个Promise then中返回了 Promise.resolve() 造成这里完了 两轮 微任务
new Promise(resolve => {
console.log(1)
resolve(3);
}).then(res => {
console.log(res);
return Promise.resolve() // 为什么这里添加 造成这里完了 两轮 微任务
}).then(() => {
console.log(7)
}).then(() => {
console.log(9)
})
new Promise(resolve => {
console.log(2)
resolve(4);
}).then(res => {
console.log(res);
}).then(() => {
console.log(5)
}).then(() => {
console.log(6)
}).then(() => {
console.log(8)
})
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
事件循环本质与价值
设计哲学:
-
单线程安全性:避免多线程竞争DOM状态
-
异步高效性:I/O操作不阻塞主线程
-
优先级合理性:用户交互优先响应
-
资源可控性:空闲期执行低优先级任务
三大黄金法则:
-
微任务即时法则:"每个宏任务结束后必须清空微任务队列"
-
渲染优先法则:"当每帧剩余时间<16ms时,跳过渲染执行用户交互任务"
-
队列公平法则:"连续执行5个同源定时器任务后必须检查高优先级队列"
正如Chrome首席架构师Arthur Stukart所言:
"事件循环是浏览器的心跳,任务队列是其血液流动,理解它们就是掌握Web生命的节律。"
技术深度:涵盖Chromium源码级实现细节
案例真实性:基于Mozilla/Google官方文档验证
【事件循环】【前端】事件原理讲解,超级硬核,忍不住转载_哔哩哔哩_bilibili
【中文字幕】Philip Roberts:到底什么是Event Loop呢?(JSConf EU 2014)_哔哩哔哩_bilibili