前言
JavaScript 单线程设计的背后,是浏览器强大的多进程、多线程架构支持。我们来看一下浏览器如何配合 JavaScript 实现高效的事件循环和页面渲染。
一、浏览器的多进程架构
现代浏览器(如 Chrome)采用多进程 + 多线程架构:
-
主进程:负责管理标签页、插件等。
-
每个标签页是一个独立的渲染进程:
- 防止页面崩溃影响其他页面。
- 提升安全性和稳定性。
渲染进程的主要职责:
- 解析 HTML 构建 DOM 树。
- 解析 CSS 构建 CSSOM 树。
- 构建渲染树(Render Tree)。
- 布局(Layout)计算元素位置。
- 绘制(Paint)到屏幕。
- 合成(Composite)图层提交给 GPU。
这些步骤都由主线程完成,而 JS 也是在这条主线程上运行的。
二、JS 与渲染互斥
由于 JS 和页面渲染都在同一线程中执行,因此它们是互斥的:
- 当 JS 正在执行时,页面无法渲染。
- 当页面正在渲染时,JS 无法执行。
这也是为何 长时间的同步任务会导致页面“卡死” 的原因。
三、事件队列与异步线程
虽然 JS 是单线程的,但浏览器为一些耗时任务提供了专属线程来处理:
| 异步任务类型 | 是否有专属线程 | 说明 |
|---|---|---|
setTimeout / setInterval | ✅ 有定时器线程 | 到时间后将回调放入宏任务队列 |
fetch / XHR | ✅ 有网络线程 | 请求完成后将回调放入宏任务队列 |
addEventListener | ❌ 没有 | 直接注册到事件队列中 |
四、事件循环机制详解
1. 宏任务(Macrotask)
-
每次事件循环处理一个宏任务。
-
宏任务包括:
- 整个
<script>脚本 setTimeoutsetInterval- 用户交互事件等
- 整个
2. 微任务(Microtask)
-
在当前宏任务结束后立即执行。
-
包括:
Promise.then/catch/finallyMutationObserverqueueMicrotask- Node.js 中的
process.nextTick
3. 执行顺序
- 执行宏任务(如
<script>中的同步代码)。 - 清空微任务队列。
- 触发页面渲染。
- 进入下一轮事件循环。
📌 注意:页面渲染发生在宏任务结束、微任务清空之后。
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 代码,还能让我们更好地理解浏览器的工作机制。掌握宏任务、微任务、渲染时机之间的关系,是前端开发进阶的关键一步!