Vue3 Patch 算法深度解析:从原理到源码实现

引言

在 Vue3 中,Diff 算法(也称为 Patch 算法)是实现响应式更新的核心机制。它通过对比新旧虚拟 DOM,以最小代价更新真实 DOM,从而实现高效的视图渲染。本文将从通俗解释到源码剖析,带你深入理解 Vue3 Patch 算法的工作原理。


一、什么是 Patch 算法?

通俗理解

想象一下,你有两张图片:一张是当前屏幕显示的旧图片(旧 VNode),另一张是需要显示的新图片(新 VNode)。Patch 算法就像是一个精明的画家,他不会把整张画布重新涂一遍,而是只修改需要改变的部分。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    旧 VNode 树                              │
│   <div>                                                    │
│     <p>Hello</p>     ──────┐                               │
│     <span>World</span>      │  对比差异                     │
│   </div>                    │                               │
└─────────────────────────────┼───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    新 VNode 树                              │
│   <div>                                                    │
│     <p>Hi</p>           ← 只修改文字                        │
│     <span>World</span>  ← 无需修改                          │
│     <button>Click</button> ← 新增节点                       │
│   </div>                                                   │
└─────────────────────────────────────────────────────────────┘

核心目标

  1. 最小化 DOM 操作:只更新变化的部分,避免不必要的重绘
  2. 保持组件状态:复用已存在的 DOM 节点,保留其状态
  3. 高效对比:采用分层对比策略,时间复杂度 O(n)

二、Patch 算法的核心流程

整体流程图

复制代码
新 VNode                    旧 VNode
    │                          │
    │                          │
    ▼                          ▼
┌───────────┐          ┌───────────┐
│ 对比类型   │          │          │
│ (sameVNode)│          │          │
└─────┬─────┘          └─────┬─────┘
      │                      │
      │ 类型不同             │
      │ (tag/key)           │
      ▼                      ▼
┌───────────┐          ┌───────────┐
│ 替换节点   │          │ 复用节点   │
│ (replace) │          │ (patch)   │
└─────┬─────┘          └─────┬─────┘
      │                      │
      │                      │
      └──────────┬───────────┘
                 │
                 ▼
           ┌───────────┐
           │ 更新属性   │
           │ (patchProps)│
           └─────┬─────┘
                 │
                 ▼
           ┌───────────┐
           │ 更新子节点 │
           │ (patchChildren)│
           └───────────┘

关键判断条件

typescript 复制代码
// 判断两个节点是否可以复用(sameVNode)
function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  return (
    n1.type === n2.type &&      // 标签类型相同
    n1.key === n2.key &&        // key 值相同
    // ... 其他条件
  )
}

三、Patch 算法的核心场景

场景一:节点类型不同 → 直接替换

当新旧节点类型不同时,直接删除旧节点并创建新节点:

typescript 复制代码
// packages/runtime-core/src/renderer.ts
const patch = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  ...
) => {
  if (n1 && !isSameVNodeType(n1, n2)) {
    // 类型不同,直接替换
    unmount(n1)
    n1 = null
  }
  
  const { type } = n2
  // ... 创建新节点
}

场景二:节点类型相同 → 深度对比

当节点类型相同时,需要对比并更新:

  1. 更新属性(patchProps)
  2. 更新子节点(patchChildren)
更新属性(patchProps)
typescript 复制代码
// packages/runtime-dom/src/patchProp.ts
function patchProp(
  el: Element,
  key: string,
  prevValue: any,
  nextValue: any,
  ...
) {
  if (key === 'class') {
    // 更新 class
    patchClass(el, nextValue)
  } else if (key === 'style') {
    // 更新 style
    patchStyle(el, prevValue, nextValue)
  } else if (shouldSetAsAttr(el, key, nextValue)) {
    // 更新标准属性
    setAttr(el, key, nextValue)
  } else {
    // 更新 DOM 属性
    el[key] = nextValue
  }
}
更新子节点(patchChildren)

这是 Patch 算法最核心的部分,Vue3 对此做了大量优化。


四、核心优化:Diff 算法详解

子节点对比策略

Vue3 的 Diff 算法采用了双端对比 策略,结合最长递增子序列算法,实现高效的节点复用。

双端对比(双指针法)
typescript 复制代码
// packages/runtime-core/src/renderer.ts
const patchChildren = (
  n1: VNode,
  n2: VNode,
  container: RendererElement,
  ...
) => {
  const c1 = n1.children as VNode[]
  const c2 = n2.children as VNode[]
  
  // 双端指针
  let i = 0
  let e1 = c1.length - 1
  let e2 = c2.length - 1
  
  // 1. 从头部开始对比
  while (i <= e1 && i <= e2) {
    const n1Child = c1[i]
    const n2Child = c2[i]
    if (isSameVNodeType(n1Child, n2Child)) {
      patch(n1Child, n2Child, container)
    } else {
      break
    }
    i++
  }
  
  // 2. 从尾部开始对比
  while (i <= e1 && i <= e2) {
    const n1Child = c1[e1]
    const n2Child = c2[e2]
    if (isSameVNodeType(n1Child, n2Child)) {
      patch(n1Child, n2Child, container)
    } else {
      break
    }
    e1--
    e2--
  }
  
  // 3. 新节点更多 → 添加新节点
  if (i > e1) {
    while (i <= e2) {
      patch(null, c2[i], container)
      i++
    }
  }
  
  // 4. 旧节点更多 → 删除多余节点
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i])
      i++
    }
  }
  
  // 5. 中间乱序 → 最长递增子序列
  else {
    // ... 见下文
  }
}
最长递增子序列(LIS)

当中间节点顺序混乱时,Vue3 使用最长递增子序列算法找到可以复用的节点:

typescript 复制代码
// 示例:旧节点顺序 [a, b, c, d, e]
//       新节点顺序 [a, c, b, e, f]

// 构建索引映射表
const keyToNewIndexMap: Map<string | number, number> = new Map()
for (i = s2; i <= e2; i++) {
  keyToNewIndexMap.set(c2[i].key!, i)
}

// 找到最长递增子序列
// 在这个例子中:[a, c, e] 是最长递增子序列
// 它们的索引在新数组中是递增的:0, 1, 3

// 这些节点可以保持不动,其他节点需要移动

最长递增子序列算法(O(n log n)):

typescript 复制代码
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  
  for (let i = 0; i < arr.length; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      const last = result[result.length - 1]
      if (arr[last] < arrI) {
        p[i] = last
        result.push(i)
        continue
      }
      
      let lo = 0, hi = result.length - 1
      while (lo < hi) {
        const mid = (lo + hi) >> 1
        if (arr[result[mid]] < arrI) {
          lo = mid + 1
        } else {
          hi = mid
        }
      }
      
      if (arrI < arr[result[lo]]) {
        if (lo > 0) {
          p[i] = result[lo - 1]
        }
        result[lo] = i
      }
    }
  }
  
  let i = result.length
  let last = result[i - 1]
  while (i-- > 0) {
    result[i] = last
    last = p[last]
  }
  
  return result
}

五、通俗解释:Diff 算法是如何工作的?

类比:整理书架

想象你是一个图书管理员,需要按照新的图书列表重新整理书架:

  1. 从左到右检查:如果书的位置和内容都对,就不用动;

  2. 从右到左检查:同样的道理,末尾的书如果已经正确,也不用动;

  3. 新书上架:如果新列表有多余的书,直接添加到合适位置;

  4. 旧书下架:如果旧列表有多余的书,把它们移除;

  5. 整理中间:对于中间乱序的书,找到可以保持不动的最长序列,只移动需要调整的书。

    旧书架:[A, B, C, D, E]
    新书架:[A, C, B, E, F]

    步骤:

    1. 从左检查:A 正确,保持不动
    2. 从右检查:E 正确,保持不动
    3. 添加新书:F 需要添加
    4. 删除旧书:D 需要删除
    5. 整理中间:C 和 B 需要交换位置

为什么要找最长递增子序列?

因为最长递增子序列中的元素在新数组中是按顺序排列的,它们不需要移动。只需要移动不在这个序列中的元素,这样可以最小化 DOM 操作次数。


六、Vue3 vs Vue2 Diff 算法对比

特性 Vue2 Vue3
对比策略 双端对比 双端对比 + 最长递增子序列
时间复杂度 O(n) ~ O(n²) O(n log n)
优化重点 简单场景优化 复杂场景优化
静态节点提升 有(PatchFlags)
Fragment 支持 有限 完整支持

Vue3 的 PatchFlags 优化

Vue3 引入了 PatchFlags,在编译阶段标记节点的更新类型:

typescript 复制代码
// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
  TEXT = 1,           // 文本内容更新
  CLASS = 2,          // class 更新
  STYLE = 4,          // style 更新
  PROPS = 8,          // 属性更新
  FULL_PROPS = 16,    // 所有属性更新
  HYDRATE_EVENTS = 32,// 事件监听器更新
  STABLE_FRAGMENT = 64,
  KEYED_FRAGMENT = 128,
  UNKEYED_FRAGMENT = 256,
  NEED_PATCH = 512,
  DYNAMIC_SLOTS = 1024,
  HOISTED = -1,       // 静态提升
  BAIL = -2           // 退出对比
}

七、实战演示:Diff 算法效果

示例代码

vue 复制代码
<template>
  <div>
    <div v-for="item in list" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const list = ref([
  { id: 1, name: 'A' },
  { id: 2, name: 'B' },
  { id: 3, name: 'C' }
])

// 模拟数据变化
setTimeout(() => {
  list.value = [
    { id: 1, name: 'A' },
    { id: 3, name: 'C' },
    { id: 2, name: 'B' },
    { id: 4, name: 'D' }
  ]
}, 2000)
</script>

Diff 过程分析

复制代码
旧列表:[A(1), B(2), C(3)]
新列表:[A(1), C(3), B(2), D(4)]

对比过程:
1. 头部对比:A 相同,继续
2. 尾部对比:C 和 B 不同,停止
3. 新列表更长:需要添加 D
4. 中间乱序:C 和 B 需要交换

最长递增子序列:[A, C]
→ A 和 C 保持不动
→ B 需要移动到 C 后面
→ D 需要添加

八、总结

Vue3 的 Patch 算法通过以下策略实现高效的 DOM 更新:

  1. 双端对比:从两端向中间对比,快速定位不变的节点;
  2. 最长递增子序列:最小化节点移动次数;
  3. PatchFlags:编译期优化,精确标记更新类型;
  4. 静态提升:避免不必要的 diff。

理解 Patch 算法有助于我们写出更高效的 Vue 代码,特别是在处理大量列表数据时,合理设置 key 值可以显著提升性能。

相关推荐
streaker3031 小时前
从复制 Token 到复用登录态:site-fetchkit 的抽离过程
前端·浏览器·ai编程
卤蛋fg61 小时前
vxe-table 导出 XLSX 文件:自动展开分组并导出图片
vue.js
dsyyyyy11011 小时前
CSS继承性
前端·css·tensorflow
wordbaby1 小时前
React Native 压缩上传全链路方案:从架构设计到生产实践
前端·react native
Rain5091 小时前
05. mini-cc 工具系统:让 AI 拥有动手能力
linux·前端·人工智能·ubuntu·typescript·ai编程
mengqudoh1 小时前
elementui el-table 表头固定功能
javascript·vue.js·elementui
YiWait1 小时前
基于 Vue 3 的网络收音机,编译为桌面应用软件
前端·javascript·vue.js
虾壳云官方2 小时前
OpenClaw 绑定企业微信完整指南
服务器·前端·网络·人工智能·企业微信·open claw·小龙虾
MichaelJohn2 小时前
别卷框架了!前端人,用 JS + LangChain + DeepSeek 开启你的 AI 转型第一步
前端