Appearance
要更好地记住和区分 宏任务(Macro-task) 与 微任务(Micro-task),可以从以下几个角度入手:本质区别、执行顺序、记忆口诀、生活类比。下面为你系统梳理:
🔑 一、核心区别(本质)
| 特性 | 宏任务(Macro-task) | 微任务(Micro-task) |
|---|---|---|
| 执行时机 | 每次事件循环(event loop)只执行一个 | 在当前宏任务执行完后、下一个宏任务开始前,全部执行完 |
| 优先级 | 低 | 高 |
| 队列机制 | 多个宏任务排队,逐个执行 | 一个宏任务结束后,清空整个微任务队列 |
| 典型代表 | setTimeout, setInterval, I/O, UI渲染, <script>整体代码 | Promise.then/catch/finally, queueMicrotask, MutationObserver, Node.js 中的 process.nextTick(优先级最高) |
✅ 关键口诀: “宏任务一个一个来,微任务一次清空完。”
🧠 二、记忆技巧
1. 首字母联想
- Macro → Main / Major → 主流程,大块任务(如整个 script、定时器)
- Micro → Mini / Micro → 小而快,用于“立即响应”的异步操作(如 Promise 回调)
2. 执行顺序口诀(重点!)
“同步代码 → 微任务 → 宏任务 → 微任务 → 宏任务……”
每次事件循环的步骤:
- 执行一个宏任务(比如 script 或 setTimeout 回调)
- 执行所有当前微任务(直到队列为空)
- 渲染 UI(浏览器环境)
- 取下一个宏任务,重复
3. 常见例子归类表
| 类型 | 宏任务(Macro) | 微任务(Micro) |
|---|---|---|
| 浏览器 | setTimeout, setInterval, requestAnimationFrame, I/O, 整个 <script> | Promise.then/catch, queueMicrotask, MutationObserver |
| Node.js | setTimeout, setInterval, I/O, setImmediate | process.nextTick(⚠️ 比 Promise 更高!), Promise |
💡 注意:
process.nextTick虽然属于微任务,但在 Node.js 中它比 Promise 更早执行,是“微任务中的 VIP”。
🌰 三、经典例子加深理解
js
console.log('1');
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4');输出顺序:1 → 4 → 3 → 2
解释:
- 同步代码:1, 4
- 当前宏任务(script)结束 → 执行所有微任务 → 输出 3
- 下一轮事件循环 → 执行 setTimeout 回调 → 输出 2
🧩 四、生活类比(帮助理解)
想象你在餐厅吃饭:
- 宏任务 = 点一道菜(每道菜要等上一轮吃完才能上下一道)
- 微任务 = 吃完这道菜后,服务员立刻给你加的免费小菜或调料(必须在下一道主菜前全部上完)
你不能一边吃主菜一边等下一道主菜;但主菜吃完后,所有“小菜”(微任务)必须先全部上完,才能上下一道主菜(下一个宏任务)。
✅ 总结记忆要点
- 微任务优先级高于宏任务
- 一个宏任务 → 清空所有微任务 → 下一个宏任务
- Promise 是微任务,setTimeout 是宏任务(最容易混淆的点!)
- script 整体代码也是一个宏任务(很多人忽略!)
掌握这些,你就能轻松应对面试题和实际开发中的异步逻辑问题!🎯
非常好的问题!要完整理解浏览器从接收到网页代码到最终显示内容的全过程,JavaScript 解析器(JS Engine) 是不可或缺的一环。它不仅负责执行脚本,还会动态影响 DOM、CSSOM 甚至渲染流程本身。
下面我将为你提供一个 完整、系统、现代浏览器视角下的“从解析到渲染”全流程,包含 HTML、CSS、JavaScript 三大解析器 以及它们如何协同工作。
🌐 一、浏览器核心组件概览(完整版)
现代浏览器(以 Chromium 为例)主要包含以下核心引擎/模块:
| 模块 | 负责内容 | 典型实现 |
|---|---|---|
| HTML 解析器 | 解析 HTML,构建 DOM 树 | Blink(HTML Parser) |
| CSS 解析器 | 解析 CSS,构建 CSSOM 树 | Blink(CSS Parser) |
| JavaScript 引擎 | 解析并执行 JS 代码 | V8(Chrome/Edge)、SpiderMonkey(Firefox)、JavaScriptCore(Safari) |
| 渲染引擎 | 布局、绘制、合成 | Blink(含 Layout、Paint、Compositor) |
| 网络模块 | 加载 HTML/CSS/JS/图片等资源 | Chromium Network Stack |
| 合成器 & GPU 模块 | 高效合成图层,GPU 加速 | Viz(Chromium)、WebRender(Firefox) |
✅ 关键点:JS 引擎不是孤立的,它与 HTML/CSS 解析器深度交互,甚至能中断或重启渲染流程。
🔁 二、完整渲染流程(含 JavaScript)
以下是浏览器加载一个网页时的 端到端流程,共 8 个核心阶段,JS 的介入贯穿始终:
1️⃣ 网络请求与字节流接收
- 浏览器发起 HTTP(S) 请求,接收 HTML 字节流。
- 字节流通过 编码解码器(Decoder) 转为字符流(如 UTF-8)。
2️⃣ HTML 解析 + 预加载扫描(Preload Scanner)
- HTML 解析器 逐段解析 HTML,构建 DOM 树。
- 同时,预加载扫描器(非阻塞)提前发现
<script>、<link>、<img>等资源,并行发起网络请求,加速加载。
⚠️ 遇到
<script>标签时:
- 若无
async/defer:阻塞 HTML 解析,等待 JS 下载并执行。- 若有
async:异步下载,下载完立即执行(可能中断解析)。- 若有
defer:异步下载,等 DOM 解析完再执行。
3️⃣ JavaScript 解析与执行(关键介入点)
当遇到 JS 脚本时,触发 JS 引擎 工作:
JS 引擎内部流程(以 V8 为例):
- 词法分析 & 语法分析 → 生成 AST(抽象语法树)
- 字节码生成(Ignition 解释器)
- 热点代码优化(TurboFan 编译器 → 生成机器码)
- 执行 JS 代码
JS 对渲染的影响:
- 修改 DOM:
document.createElement()、innerHTML等 → 触发 DOM 树变更。 - 修改 CSS:
element.style.color = 'red'或classList.add()→ 触发样式重新计算。 - 读取布局信息:如
offsetHeight、getComputedStyle()→ 强制同步布局(Layout Thrashing)。 - 创建新资源:动态插入
<script>或fetch()→ 触发新网络请求。
✅ 重要:JS 执行期间,HTML 解析暂停(除非
async/defer),渲染无法继续。
4️⃣ CSS 解析与 CSSOM 构建
- 并行下载的 CSS 文件被 CSS 解析器 解析为 CSSOM 树(样式规则结构化)。
- CSS 是阻塞渲染的:浏览器不会在 CSSOM 构建完成前开始渲染(避免“无样式内容闪烁” FOUC)。
💡 注意:JS 执行时若依赖样式(如
getComputedStyle),必须等 CSSOM 构建完成。
5️⃣ 样式计算(Style Recalculation)
- 将 DOM + CSSOM 结合,为每个 DOM 节点计算最终的 Computed Style。
- 输出一棵 Render Tree(或称 Style Tree),只包含可见元素(
display: none的元素不包含)。
🔄 如果 JS 修改了 class 或 style,此阶段会重新触发。
6️⃣ 布局(Layout / Reflow)
- 根据 Render Tree 和视口尺寸,计算每个元素的 几何信息(位置、宽高)。
- 输出 Layout Tree(Box Tree)。
- 开销大,应尽量避免频繁触发。
🔄 JS 读取
offsetTop等布局属性会强制立即执行布局(即使还没到渲染阶段)。
7️⃣ 绘制(Paint)与光栅化(Rasterization)
- 将 Layout Tree 分解为 绘制指令(如“画一个红色矩形”)。
- 光栅化:将指令转为像素位图(CPU 或 GPU)。
- 页面被划分为 图块(Tiles),按需绘制和缓存。
8️⃣ 合成(Compositing)与显示
- 合成器将多个图层(如滚动层、视频层)GPU 合成为最终帧。
- 通过 双缓冲、垂直同步(VSync) 机制,以 60fps(或更高)显示到屏幕。
🧩 三、JS 如何打断渲染流程?(关键交互点)
| 场景 | 影响 |
|---|---|
<script> 无属性 | 阻塞 HTML 解析,延迟 DOM 构建 |
| JS 修改 DOM | 触发样式重算 + 布局 + 绘制 |
| JS 读取布局属性 | 强制同步执行布局(性能陷阱) |
requestAnimationFrame() | 在合成前插入自定义逻辑(动画最佳实践) |
Promise / setTimeout | 将任务推迟到下一帧,避免阻塞渲染 |
🖼️ 四、完整流程图(文字版)
网络请求
↓
HTML 字节流 → [HTML Parser] → DOM 树
│ ↑
│ └── 遇到 <script> → 暂停解析 → [JS Engine]
│ ↓
│ 执行 JS → 可能修改 DOM/CSS
↓
CSS 文件 → [CSS Parser] → CSSOM 树
↓
DOM + CSSOM → [Style Engine] → Render Tree(带样式的可见节点)
↓
[Layout Engine] → 计算位置/尺寸 → Layout Tree
↓
[Paint] → 生成绘制指令 → [Raster] → 位图(图块)
↓
[Compositor] → 合成图层 → GPU → 屏幕🚀 五、现代浏览器优化(多线程 & 流水线)
为提升性能,现代浏览器采用 多进程/多线程架构:
- 主线程(Main Thread):HTML 解析、JS 执行、样式计算、布局。
- 光栅化线程(Raster Thread):在后台将绘制指令转为位图。
- 合成线程(Compositor Thread):独立于主线程,处理滚动、动画合成。
- 网络线程:并行下载资源。
✅ 这使得即使 JS 阻塞主线程,滚动或 CSS 动画仍可流畅运行(如果它们在合成层上)。
✅ 总结:完整渲染流程包含三大解析器
| 解析器 | 作用 | 与其他模块交互 |
|---|---|---|
| HTML 解析器 | 构建 DOM | 被 JS 阻塞;触发资源加载 |
| CSS 解析器 | 构建 CSSOM | 阻塞渲染;JS 可能依赖它 |
| JS 引擎 | 执行脚本 | 可修改 DOM/CSSOM,触发重排重绘 |
🌟 核心思想:浏览器渲染不是线性单向过程,而是一个 动态、可中断、可回溯的事件驱动系统,JS 是其中最活跃的“搅局者”也是“控制者”。
如果你想深入了解某一部分(比如 V8 如何优化 JS、合成层如何创建、或如何避免 Layout Thrashing),欢迎继续提问!
defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer。
通过审时度势地向 script 标签添加 async/defer,我们就可以告诉浏览器在等待脚本可用期间不阻止其它的工作,这样可以显著提升性能。