Vue3 Patch 全过程

107 阅读3分钟

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[卸载旧节点<br>挂载新节点]
    K --> L[结束]
    
    J -->|是| M[进入核心 Patch 逻辑]
    
    subgraph M [Patch 核心流程]
        M1[检查 PatchFlag] --> M2{有 dynamicChildren?}
        M2 -->|有| M3[Block Tree Diff<br>只比较动态节点]
        M2 -->|无| M4{有 PatchFlag?}
        M4 -->|有| M5[靶向更新<br>根据标记更新]
        M4 -->|无| M6[全量 Diff<br>Vue2 方式]
    end
    
    M3 --> N[执行子节点 Diff]
    M5 --> N
    M6 --> N
    
    subgraph N [子节点 Diff 流程]
        N1[预处理:<br>跳过相同首尾] --> 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 执行更新组件新旧 VNodePatch 调用patch(n1, n2, container)

阶段 2:Patch 入口决策

graph LR
    A[Patch 开始] --> B{新旧节点类型相同?}
    B -->|否| C[卸载旧节点<br>类型: n1.type]
    C --> D[挂载新节点<br>类型: n2.type]
    D --> Z[结束]
    
    B -->|是| E[检查 ShapeFlag<br>确定节点类型]
    
    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)

// 实际执行流程
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[挂载新节点<br>位置: i 到 e2]
    D -->|旧节点有剩余| F[卸载旧节点<br>位置: i 到 e1]
    D -->|双方都有剩余| G[复杂 Diff]
    
    subgraph G [复杂 Diff 流程]
        G1[建立 key-to-index 映射] --> G2[遍历旧节点]
        G2 --> G3{key 存在?}
        G3 -->|是| G4[更新节点<br>记录新索引位置]
        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) 计算过程

原始数组: [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 diffO(n)类似 Vue2
混合静态/动态Block Tree 隔离O(动态节点数)跳过静态节点

4. 关键数据结构示例

VNode 结构

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
  }
}

新旧节点映射表

旧节点: [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. 实际代码执行流程

// 示例:更新一个列表组件
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