Skip to content

要更好地记住和区分 宏任务(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. 执行顺序口诀(重点!)

“同步代码 → 微任务 → 宏任务 → 微任务 → 宏任务……”

每次事件循环的步骤:

  1. 执行一个宏任务(比如 script 或 setTimeout 回调)
  2. 执行所有当前微任务(直到队列为空)
  3. 渲染 UI(浏览器环境)
  4. 取下一个宏任务,重复

3. 常见例子归类表

类型宏任务(Macro)微任务(Micro)
浏览器setTimeout, setInterval, requestAnimationFrame, I/O, 整个 <script>Promise.then/catch, queueMicrotask, MutationObserver
Node.jssetTimeout, setInterval, I/O, setImmediateprocess.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

🧩 四、生活类比(帮助理解)

想象你在餐厅吃饭:

  • 宏任务 = 点一道菜(每道菜要等上一轮吃完才能上下一道)
  • 微任务 = 吃完这道菜后,服务员立刻给你加的免费小菜或调料(必须在下一道主菜前全部上完)

你不能一边吃主菜一边等下一道主菜;但主菜吃完后,所有“小菜”(微任务)必须先全部上完,才能上下一道主菜(下一个宏任务)。


✅ 总结记忆要点

  1. 微任务优先级高于宏任务
  2. 一个宏任务 → 清空所有微任务 → 下一个宏任务
  3. Promise 是微任务,setTimeout 是宏任务(最容易混淆的点!)
  4. 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 为例):

  1. 词法分析 & 语法分析 → 生成 AST(抽象语法树)
  2. 字节码生成(Ignition 解释器)
  3. 热点代码优化(TurboFan 编译器 → 生成机器码)
  4. 执行 JS 代码

JS 对渲染的影响:

  • 修改 DOMdocument.createElement()innerHTML 等 → 触发 DOM 树变更。
  • 修改 CSSelement.style.color = 'red'classList.add() → 触发样式重新计算。
  • 读取布局信息:如 offsetHeightgetComputedStyle()强制同步布局(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,我们就可以告诉浏览器在等待脚本可用期间不阻止其它的工作,这样可以显著提升性能。