Vue3 Patch 全过程

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

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

阶段 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

阶段 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
相关推荐
孟祥_成都2 小时前
nest.js / hono.js 一起学!字节团队如何配置多环境攻略!
前端·node.js
用户4099322502122 小时前
Vue3数组语法如何高效处理动态类名的复杂组合与条件判断?
前端·ai编程·trae
山里看瓜2 小时前
解决 iOS 上 Swiper 滑动图片闪烁问题:原因分析与最有效的修复方式
前端·css·ios
Java水解2 小时前
前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践
前端·后端
软件技术NINI2 小时前
前端怎么学
前端
O***p6042 小时前
前端体验的下一次革命:从页面导航到“流式体验”的系统化重构
前端·重构
一岁天才饺子2 小时前
XSS挑战赛实战演练
前端·网络安全·xss
Hilaku2 小时前
Canvas 粒子特效:带你写一个黑客帝国同款的代码雨(附源码)😆
前端·javascript·前端框架
文心快码BaiduComate3 小时前
我用文心快码Spec 模式搓了个“pre作弊器”,妈妈再也不用担心我开会忘词了(附源码)
前端·后端·程序员