文章目录
- 前言
- 一、什么是虚拟 DOM
- 1.1 定义
- 1.2 为什么需要
- 1.3 并非总是更快
- 二、VNode 结构
- 2.1 基本字段
- 2.2 常见类型
- 三、更新流程
- 四、Vue 2 双端 Diff
- 4.1 算法思路
- 4.2 特点
- 五、Vue 3 快速 Diff
- 5.1 为什么放弃双端 Diff
- 5.2 快速 Diff 流程(列表)
- 5.3 最长递增子序列(LIS)
- 六、Vue 3 编译时优化(概览)
- 6.1 PatchFlag:标记动态节点
- 6.2 Block Tree:扁平化动态节点
- 6.3 静态提升
- 七、key 与 Diff 的关系(简述)
- 八、与 React 的对比
- 九、面试聚焦
- 9.1 虚拟 DOM 并非总是更快
- 9.2 Vue 3 为什么改 Diff?
- 9.3 PatchFlag 做什么?
- 9.4 Block Tree 是什么?
- 十、易混淆点
- 十一、思考与练习
- 总结
前言
虚拟 DOM 是 Vue 渲染层的核心机制:用 JavaScript 对象描述 DOM,通过 Diff 算法找出最小变更并批量更新。本篇会讲清楚:
- VNode 结构与更新流程
- Vue 2 双端 Diff vs Vue 3 快速 Diff
- 最长递增子序列(LIS)与编译时优化概览
一、什么是虚拟 DOM
1.1 定义
虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级表示。状态变化时生成新的 VNode 树,与旧树 Diff 后,只把差异应用到真实 DOM。
// 真实 DOM<divclass="box"><h1>Hello</h1></div>// 虚拟 DOM(VNode 示意)constvnode={type:'div',props:{class:'box'},children:[{type:'h1',children:'Hello'}]}1.2 为什么需要
| 问题 | 虚拟 DOM 的解决 |
|---|---|
| 直接操作 DOM 慢且难追踪 | 声明式描述 UI,框架算最优更新 |
| 多次状态变更多次 DOM 操作 | 合并为一次批量更新 |
| 跨平台(Web / SSR / 小程序) | 同一套 VNode 可对接不同渲染器 |
1.3 并非总是更快
极少量的 DOM 更新时,手动改 DOM 可能更快。虚拟 DOM 的价值在于复杂场景下的声明式开发、批量更新和跨平台抽象,而不是「一定比原生 DOM 快」。
二、VNode 结构
2.1 基本字段
constvnode={type:'div',// 标签名、组件、Fragment 等props:{class:'box'},children:[],key:'unique-id',// 列表 Diff 用el:null,// 运行时关联的真实 DOMpatchFlag:0,// Vue 3 编译时标记(见下文)dynamicChildren:null// Vue 3 Block Tree 动态子节点}2.2 常见类型
// 文本{type:Text,children:'hello'}// 元素{type:'div',props:{},children:[]}// 组件{type:MyComponent,props:{msg:'hi'}}// Fragment(多根节点){type:Fragment,children:[...]}三、更新流程
响应式数据变化 ↓ 触发组件 re-render,生成新 VNode 树 ↓ 新旧 VNode 树 Diff(patch) ↓ 计算出最小变更集(增删改移) ↓ 批量应用到真实 DOM// 简化 Diff 思路functionpatch(oldVNode,newVNode){if(oldVNode.type!==newVNode.type){// 类型不同 → 替换节点replaceNode(oldVNode,newVNode)return}// 同类型 → 比 props、比 childrenpatchProps(oldVNode,newVNode)patchChildren(oldVNode,newVNode)}四、Vue 2 双端 Diff
4.1 算法思路
Vue 2 对同级列表使用双端比较:新旧数组各设头尾指针,从两端向中间同时比较,尽量复用 DOM。
旧: A B C D ↑ ↑ oldStart oldEnd 新: D A B C ↑ ↑ newStart newEnd 比较顺序(共 4 种): 1. oldStart vs newStart 2. oldEnd vs newEnd 3. oldStart vs newEnd 4. oldEnd vs newStart 都不匹配 → 用 key 在旧列表中查找4.2 特点
- 适合列表头尾增删、反转等常见场景
- 依赖key做节点身份识别
- 最坏情况仍需较多比较,Vue 3 做了进一步优化
五、Vue 3 快速 Diff
5.1 为什么放弃双端 Diff
Vue 3 借鉴 Inferno 的快速 Diff,目标:
- 减少不必要的节点比较次数
- 用最长递增子序列(LIS)优化列表移动操作,少做 DOM insert/move
5.2 快速 Diff 流程(列表)
1. 从头同步:新旧节点 type + key 相同则 patch,不同则停 2. 从尾同步:同上,从尾部向前 3. 中间段:用 key → index 映射处理新增、删除、移动 4. 移动优化:对需要移动的节点求 LIS,LIS 内节点不移动,其余按需 insert5.3 最长递增子序列(LIS)
旧: [A, B, C, D, E] 新: [A, C, D, B, E] (B 从 index 1 移到 index 3) 需要移动的: B LIS 帮助找出「已经相对有序、不必动」的节点,减少 DOM 移动次数LIS 让 Diff 在「乱序但可复用」的列表里,用最少 DOM 移动完成更新。
六、Vue 3 编译时优化(概览)
Diff 之外,Vue 3 在编译阶段减少需要 Diff 的节点量:
6.1 PatchFlag:标记动态节点
编译器分析模板,给 VNode 打上「哪里会变」的标记:
// 编译结果示意createElementVNode('div',{class:'static'},createTextVNode('hello',PatchFlags.TEXT)// 仅文本会变)| PatchFlag | 含义 |
|---|---|
| TEXT (1) | 动态文本 |
| CLASS (2) | 动态 class |
| STYLE (4) | 动态 style |
| PROPS (8) | 动态 props |
| … | 组合标记 |
Diff 时若 patchFlag 为 0,可跳过该节点子树比较;有标记则只比较标记部分。
6.2 Block Tree:扁平化动态节点
将模板中的动态节点收集到dynamicChildren数组,Diff 时只遍历动态节点,静态子树整段跳过。
<div> <p>静态标题</p> <!-- 静态,不参与 Diff --> <p>{{ msg }}</p> <!-- 动态,进入 dynamicChildren --> <span>{{ count }}</span> <!-- 动态,进入 dynamicChildren --> </div>6.3 静态提升
纯静态节点提升到 render 函数外,只创建一次,后续 render 直接复用,避免重复生成 VNode。
编译优化的细节(PatchFlag 类型、Block 收集规则等)在编译优化专题中展开。
七、key 与 Diff 的关系(简述)
列表 Diff 依赖key判断「同一节点」:
- 相同 key + 相同 type → patch(复用 DOM)
- 不同 key → 销毁旧节点,创建新节点
key 不稳定(如用 index 做增删)会导致错误复用。key 的完整原理见专题「Key 的作用与原理」。
八、与 React 的对比
| 对比项 | Vue 3 | React |
|---|---|---|
| Diff 粒度 | 组件级 + Block 内动态节点 | Fiber 可中断的增量 Diff |
| 列表算法 | 快速 Diff + LIS | 单端 + key 映射 |
| 编译优化 | PatchFlag、静态提升、Block Tree | 部分优化,策略不同 |
两者都用虚拟 DOM,但 Diff 策略和编译优化路径不同。
九、面试聚焦
9.1 虚拟 DOM 并非总是更快
简单场景手动 DOM 可能更快;虚拟 DOM 的价值是声明式、批量更新、跨平台。
9.2 Vue 3 为什么改 Diff?
双端 Diff 在复杂列表下比较次数仍偏多;快速 Diff + LIS 减少移动,配合 PatchFlag / Block Tree 减少比较范围。
9.3 PatchFlag 做什么?
编译期标记 VNode 哪些部分动态,Diff 时跳过静态内容,只更新标记位。
9.4 Block Tree 是什么?
把动态节点扁平收集,Diff 只比 dynamicChildren,静态子树整段跳过。
十、易混淆点
- 虚拟 DOM ≠ 更快:是开发模型和批量更新策略,不是性能银弹。
- Diff 只做同级比较:不会跨层级移动节点(O(n) 层级比较)。
- Vue 2 / Vue 3 列表 Diff 不同:Vue 3 用快速 Diff + LIS。
- PatchFlag 是编译产物:手写 render 函数默认无此优化。
- Shadow DOM ≠ Virtual DOM:前者是 Web Components 浏览器封装,与框架 VNode 无关。
十一、思考与练习
1.虚拟 DOM 的更新流程是什么?
解析:数据变 → 新 VNode → 与旧 VNode Diff → 最小变更 → 更新真实 DOM。
2.Vue 2 和 Vue 3 列表 Diff 有何不同?
解析:Vue 2 双端比较;Vue 3 快速 Diff,头尾同步 + 中间 key 映射 + LIS 优化移动。
3.LIS 在 Diff 中的作用?
解析:找出相对有序、不必移动的节点,减少 DOM insert/move 次数。
4.PatchFlag 和 Block Tree 解决什么问题?
解析:减少 Diff 范围——只比动态节点、只更新标记为动态的部分,静态内容跳过。
5.为什么说虚拟 DOM 不总是更快?
解析:创建 VNode 和 Diff 本身有开销;极简单更新直接改 DOM 可能更省。
总结
- 虚拟 DOM:JS 对象描述 DOM,声明式 + 批量更新 + 跨平台
- 更新流程:新 VNode → Diff → 最小 patch → 真实 DOM
- Vue 2:双端 Diff,依赖 key
- Vue 3:快速 Diff + LIS;PatchFlag、Block Tree、静态提升减少 Diff 量
- 本质:虚拟 DOM 是工程权衡,不是「一定比原生 DOM 快」