1. 完整流程图概览
graph TD
A[响应式数据变更] --> B[触发组件更新]
B --> C{是首次渲染?}
C -->|是| D[执行 mount 流程]
D --> E[创建 Block Tree]
E --> F[递归创建 VNode]
F --> G[转换为真实 DOM]
G --> H[完成挂载]
C -->|否| I[执行 patch 流程]
I --> J{新旧 VNode 类型相同?}
J -->|否| K[卸载旧节点
挂载新节点] K --> L[结束] J -->|是| M[进入核心 Patch 逻辑] subgraph M [Patch 核心流程] M1[检查 PatchFlag] --> M2{有 dynamicChildren?} M2 -->|有| M3[Block Tree Diff
只比较动态节点] M2 -->|无| M4{有 PatchFlag?} M4 -->|有| M5[靶向更新
根据标记更新] M4 -->|无| M6[全量 Diff
Vue2 方式] end M3 --> N[执行子节点 Diff] M5 --> N M6 --> N subgraph N [子节点 Diff 流程] N1[预处理:
跳过相同首尾] --> N2{还有剩余节点?} N2 -->|无| N3[结束] N2 -->|有| N4[复杂 Diff 流程] N4 --> N5[建立 key-index 映射] N5 --> N6[创建新旧索引映射表] N6 --> N7[计算最长递增子序列 LIS] N7 --> N8[移动/创建/删除节点] end N8 --> O[更新 DOM] N3 --> O O --> P[完成更新] style A fill:#f9f,stroke:#333,stroke-width:2px style H fill:#ccf,stroke:#333,stroke-width:2px style P fill:#ccf,stroke:#333,stroke-width:2px style M3 fill:#9f9,stroke:#333 style M5 fill:#9f9,stroke:#333 style M6 fill:#f99,stroke:#333 style N7 fill:#ff9,stroke:#333
挂载新节点] K --> L[结束] J -->|是| M[进入核心 Patch 逻辑] subgraph M [Patch 核心流程] M1[检查 PatchFlag] --> M2{有 dynamicChildren?} M2 -->|有| M3[Block Tree Diff
只比较动态节点] M2 -->|无| M4{有 PatchFlag?} M4 -->|有| M5[靶向更新
根据标记更新] M4 -->|无| M6[全量 Diff
Vue2 方式] end M3 --> N[执行子节点 Diff] M5 --> N M6 --> N subgraph N [子节点 Diff 流程] N1[预处理:
跳过相同首尾] --> N2{还有剩余节点?} N2 -->|无| N3[结束] N2 -->|有| N4[复杂 Diff 流程] N4 --> N5[建立 key-index 映射] N5 --> N6[创建新旧索引映射表] N6 --> N7[计算最长递增子序列 LIS] N7 --> N8[移动/创建/删除节点] end N8 --> O[更新 DOM] N3 --> O O --> P[完成更新] style A fill:#f9f,stroke:#333,stroke-width:2px style H fill:#ccf,stroke:#333,stroke-width:2px style P fill:#ccf,stroke:#333,stroke-width:2px style M3 fill:#9f9,stroke:#333 style M5 fill:#9f9,stroke:#333 style M6 fill:#f99,stroke:#333 style N7 fill:#ff9,stroke:#333
2. 详细步骤分解表
阶段 1:触发更新
| 步骤 | 输入 | 输出 | 关键逻辑 |
|---|---|---|---|
| 1.1 响应式变更 | 组件状态变化 | 触发副作用 | effect.run() |
| 1.2 调度更新 | 组件实例 | 更新任务 | queueJob(update) |
| 1.3 执行更新 | 组件新旧 VNode | Patch 调用 | patch(n1, n2, container) |
阶段 2:Patch 入口决策
graph LR
A[Patch 开始] --> B{新旧节点类型相同?}
B -->|否| C[卸载旧节点
类型: n1.type] C --> D[挂载新节点
类型: n2.type] D --> Z[结束] B -->|是| E[检查 ShapeFlag
确定节点类型] E --> F{节点类型判断} F -->|元素节点| G[patchElement] F -->|组件节点| H[patchComponent] F -->|文本节点| I[patchText] F -->|Fragment| J[patchFragment] F -->|其他类型| K[对应处理] G --> L[继续元素 Diff] H --> M[继续组件更新] style G fill:#9cf style H fill:#9cf
类型: n1.type] C --> D[挂载新节点
类型: n2.type] D --> Z[结束] B -->|是| E[检查 ShapeFlag
确定节点类型] E --> F{节点类型判断} F -->|元素节点| G[patchElement] F -->|组件节点| H[patchComponent] F -->|文本节点| I[patchText] F -->|Fragment| J[patchFragment] F -->|其他类型| K[对应处理] G --> L[继续元素 Diff] H --> M[继续组件更新] style G fill:#9cf style H fill:#9cf
阶段 3:元素节点 Diff (patchElement)
javascript
// 实际执行流程
function patchElement(n1, n2, container) {
// 3.1 复用 DOM 元素
const el = (n2.el = n1.el)
// 3.2 检查 PatchFlag
const { patchFlag, dynamicChildren } = n2
// 决策路径
if (dynamicChildren) {
// 🟢 情况1:有 Block 优化
patchBlockChildren(n1.dynamicChildren, dynamicChildren, el)
} else if (patchFlag) {
// 🟡 情况2:有 PatchFlag,靶向更新
if (patchFlag & PatchFlags.TEXT) {
// 只更新文本
hostSetElementText(el, n2.children)
}
if (patchFlag & PatchFlags.CLASS) {
// 只更新 class
hostPatchClass(el, n2.props.class)
}
// ... 其他标志检查
} else {
// 🔴 情况3:全量 Diff(Vue2 方式)
fullDiffElement(n1, n2, el)
}
}
阶段 4:子节点 Diff 详细流程
graph TD
A[开始子节点 Diff] --> B[预处理阶段]
subgraph B [预处理 - 跳过相同首尾]
B1[指针: i=0, e1=旧尾, e2=新尾] --> B2{头头相同?}
B2 -->|是| B3[i++, 继续比较]
B3 --> B2
B2 -->|否| B4{尾尾相同?}
B4 -->|是| B5[e1--, e2--, 继续比较]
B5 --> B4
B4 -->|否| C[进入核心 Diff]
end
C --> D{判断剩余情况}
D -->|新节点有剩余| E[挂载新节点
位置: i 到 e2] D -->|旧节点有剩余| F[卸载旧节点
位置: i 到 e1] D -->|双方都有剩余| G[复杂 Diff] subgraph G [复杂 Diff 流程] G1[建立 key-to-index 映射] --> G2[遍历旧节点] G2 --> G3{key 存在?} G3 -->|是| G4[更新节点
记录新索引位置] G3 -->|否| G5[卸载旧节点] G4 --> G6[构建 newIndexToOldIndexMap] G6 --> G7[计算最长递增子序列 LIS] G7 --> G8[从后向前遍历] G8 --> G9{当前位置在 LIS 中?} G9 -->|是| G10[保持不动] G9 -->|否| G11[需要移动] G11 --> G12[确定插入位置] G12 --> G13[执行 DOM 移动] end E --> H[完成更新] F --> H G13 --> H style G7 fill:#ff9 style G13 fill:#9f9
位置: i 到 e2] D -->|旧节点有剩余| F[卸载旧节点
位置: i 到 e1] D -->|双方都有剩余| G[复杂 Diff] subgraph G [复杂 Diff 流程] G1[建立 key-to-index 映射] --> G2[遍历旧节点] G2 --> G3{key 存在?} G3 -->|是| G4[更新节点
记录新索引位置] G3 -->|否| G5[卸载旧节点] G4 --> G6[构建 newIndexToOldIndexMap] G6 --> G7[计算最长递增子序列 LIS] G7 --> G8[从后向前遍历] G8 --> G9{当前位置在 LIS 中?} G9 -->|是| G10[保持不动] G9 -->|否| G11[需要移动] G11 --> G12[确定插入位置] G12 --> G13[执行 DOM 移动] end E --> H[完成更新] F --> H G13 --> H style G7 fill:#ff9 style G13 fill:#9f9
阶段 5:最长递增子序列 (LIS) 计算过程
ini
原始数组: [2, 4, 3, 5, 1, 6]
步骤1: 初始化
result = [0] # 存储索引,值: [2]
p = [0,0,0,0,0,0] # 前驱数组
步骤2: 处理索引1 (值4)
4 > 2, 所以 push: result = [0,1]
p[1] = 0
步骤3: 处理索引2 (值3)
3 < 4, 二分查找找到位置1替换: result = [0,2]
p[2] = 0
步骤4: 处理索引3 (值5)
5 > 3, push: result = [0,2,3]
p[3] = 2
步骤5: 处理索引4 (值1)
1 < 5, 二分查找找到位置0替换: result = [4,2,3]
p[4] = -1 (无前驱)
步骤6: 处理索引5 (值6)
6 > 5, push: result = [4,2,3,5]
p[5] = 3
步骤7: 回溯得到 LIS
从后向前: result = [2,3,5]
对应值: [3,5,6]
最终 LIS 长度: 3
需要移动的节点: 不在 [2,3,5] 中的索引
阶段 6:DOM 操作执行
graph TD
A[开始 DOM 操作] --> B{操作类型判断}
B -->|创建节点| C[createElement]
C --> D[设置属性/事件]
D --> E[插入到容器]
B -->|移动节点| F[获取参考节点]
F --> G[insertBefore 移动]
B -->|更新节点| H[根据 PatchFlag 更新]
H --> I[文本更新]
H --> J[属性更新]
H --> K[样式更新]
B -->|卸载节点| L[移除事件监听]
L --> M[递归卸载子节点]
M --> N[removeChild]
E --> O[完成操作]
G --> O
I --> O
J --> O
K --> O
N --> O
3. 性能优化决策矩阵
| 场景特征 | Vue3 选择策略 | 复杂度 | 优化效果 |
|---|---|---|---|
| 全静态内容 | 静态提升,跳过整个子树 | O(1) | 99%+ 跳过 |
| 少量动态属性 | PatchFlag 靶向更新 | O(动态属性数) | 80-90% 跳过 |
| 顺序稳定的列表 | Block + LIS 优化 | O(n log n) | 最少 DOM 移动 |
| 顺序完全打乱 | 全量 Keyed diff | O(n) | 类似 Vue2 |
| 混合静态/动态 | Block Tree 隔离 | O(动态节点数) | 跳过静态节点 |
4. 关键数据结构示例
VNode 结构
javascript
const vnode = {
type: 'div', // 节点类型
el: null, // 对应的 DOM 元素
children: [], // 子节点
dynamicChildren: null, // 动态子节点(Block 优化)
patchFlag: 9, // 补丁标志:TEXT(1) + PROPS(8)
shapeFlag: 17, // 形状标志:ELEMENT(1) + TEXT_CHILDREN(16)
key: 'item-1', // 用于 diff 的 key
props: { // 属性
class: 'container',
onClick: handler
}
}
新旧节点映射表
makefile
旧节点: [A, B, C, D, E]
索引: 0 1 2 3 4
新节点: [A, D, C, B, F]
索引: 0 1 2 3 4
映射表: [0, 3, 2, 1, -1]
解释:
新节点0(A) -> 旧索引0
新节点1(D) -> 旧索引3
新节点2(C) -> 旧索引2
新节点3(B) -> 旧索引1
新节点4(F) -> 旧索引-1(新增)
LIS计算: [0, 2, 4] 对应值 [0, 2, -1]
最长递增子序列: [0, 2](因为-1不是递增)
实际保持位置: 新节点0(A)和2(C)不动
需要移动: 新节点1(D)和3(B)
需要创建: 新节点4(F)
5. 实际代码执行流程
javascript
// 示例:更新一个列表组件
const oldVNode = {
type: 'ul',
children: [
{ type: 'li', key: 'a', children: 'A' },
{ type: 'li', key: 'b', children: 'B' },
{ type: 'li', key: 'c', children: 'C' },
{ type: 'li', key: 'd', children: 'D' }
]
}
const newVNode = {
type: 'ul',
children: [
{ type: 'li', key: 'a', children: 'A' },
{ type: 'li', key: 'c', children: 'C Updated' },
{ type: 'li', key: 'b', children: 'B Updated' },
{ type: 'li', key: 'e', children: 'E New' }
]
}
// 执行过程:
1. patch(oldVNode, newVNode)
2. 类型相同,进入 patchElement
3. 子节点 diff:
- 预处理:头头相同 (key='a' 相同)
- 剩余:旧 [b,c,d] vs 新 [c,b,e]
- 建立映射:{'c':2, 'b':3, 'e':4}
- 遍历旧节点:
* b: 存在,新位置1,更新文本
* c: 存在,新位置0,更新文本
* d: 不存在,卸载
- newIndexToOldIndexMap: [2, 1, -1]
- LIS: [0, 1] (值 [2, 1])
- 从后向前处理:
* e: 位置2,不在LIS,插入到b之前
* b: 位置1,在LIS,不动
* c: 位置0,在LIS,不动
4. DOM操作:
- 更新c和b的文本
- 在b之前插入e
- 移除d
6. 总结流程图
graph TB
Start[Patch 开始] --> Decision1{首次渲染?}
Decision1 -->|是| Mount[挂载流程]
Decision1 -->|否| Update[更新流程]
Mount --> CreateVNode[创建 VNode]
CreateVNode --> BuildDOM[构建 DOM]
BuildDOM --> End1[完成]
Update --> TypeCheck{类型相同?}
TypeCheck -->|否| Replace[替换节点]
TypeCheck -->|是| PatchCore[核心 Patch]
subgraph PatchCore [优化决策]
PC1{有 dynamicChildren?} -->|是| Block[Block 优化]
PC1 -->|否| PC2{有 patchFlag?}
PC2 -->|是| Targeted[靶向更新]
PC2 -->|否| Full[全量 Diff]
end
Block --> ChildDiff[子节点 Diff]
Targeted --> ChildDiff
Full --> ChildDiff
subgraph ChildDiff [子节点 Diff 算法]
CD1[预处理] --> CD2{剩余节点?}
CD2 -->|新节点有剩余| MountNew[挂载新节点]
CD2 -->|旧节点有剩余| UnmountOld[卸载旧节点]
CD2 -->|双方都有| KeyedDiff[Keyed Diff]
KeyedDiff --> BuildMap[建立映射]
BuildMap --> CalcLIS[计算 LIS]
CalcLIS --> MoveNodes[移动节点]
end
MountNew --> DOMOps[DOM 操作]
UnmountOld --> DOMOps
MoveNodes --> DOMOps
DOMOps --> End2[完成更新]
Replace --> End2
style Block fill:#9f9
style Targeted fill:#9f9
style Full fill:#f99
style CalcLIS fill:#ff9