Vue3.4中diff算法核心梳理

基于 Vue 3.4+(runtime-core)

一、组件更新链路

Plain 复制代码
响应式数据变化
↓
触发 effect(scheduler)
↓
组件 render 函数重新执行
↓
生成新的 VNode Tree
↓
patch(oldVNode, newVNode)
↓
精确更新真实 DOM

虚拟 DOM 的职责:描述 UI 结构diff 的职责:最小化 DOM 更新

二、Vue3 的虚拟 DOM 本质

Vue3 中的 VNode 是一个​高度优化的 JS 对象​:

TypeScript 复制代码
interface VNode {
  type: string | Component
  props: Record<string, any> | null
  children: string | VNode[] | null
  key: any
  el: HTMLElement | null // 对应真实 DOM

  shapeFlag: number // 节点类型位运算标识
  patchFlag: number // 告诉 diff:哪里可能变
  dynamicProps: string[] | null // 动态 ptops 列表
}

三、diff 的目标

单个 DOM 之间的对比,一般只需要对比节点类型、属性、文本内容等,而最最重要的其实是他们子节点的对比过程,因此下面主要讲解子节点的对比。

diff 的输入:

TypeScript 复制代码
oldChildren: VNode[]
newChildren: VNode[]

diff 的目标只有三个:

  1. 复用能复用的 DOM
  2. 最少的 DOM 操作
  3. 保证最终 DOM 顺序正确

diff 不关心"组件更新",只关心"同一父节点下 children 的变化"

TypeScript 复制代码
patchKeyedChildren(
  c1: VNode[], // oldChildren
  c2: VNode[], // newChildren
  container: Element
)

前提条件:

  • children 是数组
  • 并且 有 key(无 key 是另一套退化逻辑)

四、Vue3 diff 的完整阶段

Vue3 的 diff ​严格分 5 个阶段 ​,而且​顺序不能乱​:

Plain 复制代码
1️⃣ 从头同步
2️⃣ 从尾同步
3️⃣ 新节点多 → 挂载
4️⃣ 旧节点多 → 卸载
5️⃣ 中间乱序 diff(重点)

一阶段:从头同步

TypeScript 复制代码
let i = 0
while (
  i <= e1 &&
  i <= e2 &&
  isSameVNodeType(c1[i], c2[i])
) {
  patch(c1[i], c2[i])
  i++
}
Plain 复制代码
old: A B C D
new: A B E D
     ↑
  • A === A → patch
  • B === B → patch
  • C !== E → 停

为什么要做这一步?

其实我们现实业务中"头部稳定"是最常见情况:

  • 列表 append
  • 局部更新

这是一个 O(n) 的优化。

二阶段:从尾同步

TypeScript 复制代码
while (
  i <= e1 &&
  i <= e2 &&
  isSameVNodeType(c1[e1], c2[e2])
) {
  patch(c1[e1], c2[e2])
  e1--
  e2--
}
Plain 复制代码
old: A B C D
new: A E C D
         ↑
  • D === D
  • C === C

尾部稳定在 prepend / insert 场景很常见

三阶段:只剩新节点

TypeScript 复制代码
if (i > e1 && i <= e2)

说明:

  • oldChildren 已处理完
  • newChildren 还有剩余
Plain 复制代码
old: A B
new: A B C D
        ↑ ↑
TypeScript 复制代码
while (i <= e2) {
  patch(null, c2[i], container, anchor)
  i++
}

纯新增,直接 mount,零 diff 成本

四阶段:只剩旧节点

TypeScript 复制代码
if (i > e2 && i <= e1)
Plain 复制代码
old: A B C
new: A B
        ↑
TypeScript 复制代码
while (i <= e1) {
  unmount(c1[i])
  i++
}

到这里为止,90% 的列表更新已经解决了。

只有剩下"中间乱序"的情况,才进入真正的复杂 diff。

五阶段:中间乱序 diff

这里你要好好听了!!!有一步走神后面就听不懂了!!!

此时新旧元素 index 数组:

Plain 复制代码
old: [i ... e1]
new: [i ... e2]

例如:

Plain 复制代码
old: A B C D E
new: B A E C D

头和头比较,不同,停止;然后尾和尾比较,不同,停止;所以这个整体进入中间乱序 diff 的比较过程。

第 1 步:建立 newChildren 的 key → index 映射

TypeScript 复制代码
const keyToNewIndexMap = new Map()
for (let j = i; j <= e2; j++) {
  keyToNewIndexMap.set(c2[j].key, j)
}

​目的:​O(1) 数组直接查找新节点位置,避免 O(n²)

Plain 复制代码
newChildren:
index: 0   1   2   3   4
node:  B   A   E   C   D

构建 Map(node -> index):

Plain 复制代码
{
  B → 0,
  A → 1,
  E → 2,
  C → 3,
  D → 4
}

第 2 步:遍历旧节点,尝试复用

TypeScript 复制代码
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)

遍历 oldChildren:

TypeScript 复制代码
for (let j = i; j <= e1; j++) {
  const oldVNode = c1[j]
  const newIndex = keyToNewIndexMap.get(oldVNode.key)

  if (newIndex === undefined) {
    unmount(oldVNode)
  } else {
    newIndexToOldIndexMap[newIndex - i] = j + 1
    patch(oldVNode, c2[newIndex])
  }
}

关键设计点,为什么存 j + 1

Plain 复制代码
0 → 表示"新节点",所以如果存在复用节点至少为1,否则有歧义
>0 → 表示旧节点索引 + 1

遍历:

Plain 复制代码
old  : A   B   C   D   E
index: 0   1   2   3   4

构建新节点数组索引到旧节点数组索引的映射表:

Plain 复制代码
newIndexToOldIndexMap = []
  • 数组长度 = newChildren 中"乱序区间"的长度
  • 下标 = newIndex(新节点的位置)
  • 值 = oldIndex + 1

第 3 步:判断是否需要移动(moved 标记)

TypeScript 复制代码
if (newIndex < maxNewIndexSoFar) {
  moved = true
} else {
  maxNewIndexSoFar = newIndex
}

如果 newIndex 出现逆序,说明顺序乱了。这是什么意思呢?看下面示例演示:

依次遍历旧节点:

遍历 old[0] = A

  • 在 new 中 index = 1
  • 记录:0 + 1 = 1(oldIndex+1)

newIndexToOldIndexMap: [_, 1, _, _, _]

记录索引为 1, 对应这 newIndex(新节点的索引位置)

遍历 old[1] = B

  • newIndex = 0
  • 记录:1 + 1 = 2

[2, 1, _, _, _]

👉 **这里已经出现逆序(2>1)**→ moved = true

遍历 old[2] = C

  • newIndex = 3
  • 记录:2 + 1 = 3

[2, 1, _, 3, _]

遍历 old[3] = D

  • newIndex = 4
  • 记录:3 + 1 = 4

[2, 1, _, 3, 4]

遍历 old[4] = E

  • newIndex = 2
  • 记录:4 + 1 = 5

[2, 1, 5, 3, 4]

最终结果:newIndexToOldIndexMap = [2, 1, 5, 3, 4]

含义:

如果你🫵看到这里可以完全看懂,那么恭喜你,第一个难点已经攻破。

第 4 步:计算最长递增子序列(LIS)

只有在:

TypeScript 复制代码
if (moved) {
  const increasingNewIndexSequence =
    getSequence(newIndexToOldIndexMap)
}
Plain 复制代码
newIndexToOldIndexMap:
[2, 1, 5, 3, 4]

LIS = [1, 3, 4] // 对应的是 A -> C -> D

LIS 对应的节点:

  • 相对顺序已经正确
  • 不需要移动 DOM(A -> C -> D)

第 5 步:倒序遍历,执行 DOM 操作

TypeScript 复制代码
for (let j = toBePatched - 1; j >= 0; j--) {
  const newIndex = j + i
  const newVNode = c2[newIndex]
  const anchor = nextIndex < c2.length
    ? c2[nextIndex].el
    : null

  if (newIndexToOldIndexMap[j] === 0) {
    patch(null, newVNode, container, anchor)
  } else if (moved) {
    if (!isInLIS(j)) {
      move(newVNode, container, anchor)
    }
  }
}

为什么要 ​倒序 ​?保证 anchor 永远是稳定的 DOM。

倒序处理 newChildren:

D → C → E → A → B

1️⃣ 处理 D(在 LIS)

👉 不动

DOM 还是:

A B C D E

2️⃣ 处理 C(在 LIS)

👉 不动

3️⃣ 处理 E(❌ 不在 LIS)

👉 移动 E 到 C 前面

A B E C D

4️⃣ 处理 A(在 LIS)

👉 不动

5️⃣ 处理 B(❌ 不在 LIS)

👉 移动 B 到 A 前面

B A E C D

最终 DOM 结构达到正确。

LIS 节点就像一排站得顺序已经对的柱子,Vue3 只需要把站错位置的挪到正确的位置。

五、Vue3 diff 的完整决策树

Plain 复制代码
children diff
│
├─ 头部同步
├─ 尾部同步
├─ 纯新增
├─ 纯删除
└─ 中间乱序
   ├─ key → index 映射
   ├─ 旧节点复用 / 卸载
   ├─ 判断是否需要移动
   ├─ LIS
   └─ 倒序 mount / move

复杂度分析:

​Vue3 diff 的最坏复杂度:O(n log n),​ 但大部分真实场景 接近 O(n)。

为什么 Vue3 diff 比 Vue2 强?

  1. 阶段化 diff(不是全量递归)
  2. LIS 最小移动
  3. 编译期 patchFlag 极大减少进入 diff 的节点数量
  4. Block Tree 让 diff 只遍历"动态节点"

六、Vue3 为什么比 Vue2 diff 快?

编译期 patchFlag

模板:

HTML 复制代码
<div>{{ count }}</div>

编译后:

JavaScript 复制代码
createElementVNode("div", null, count, 1 /* TEXT */)

patchFlag 告诉 diff:"只需要比对文本"

跳过 props / children / key

dynamicProps 精确比对

HTML 复制代码
<div :id="id" :class="cls"></div>
JavaScript 复制代码
dynamicProps = ["id", "class"]

不再遍历所有 props

静态提升(hoistStatic)

JavaScript 复制代码
const _hoisted_1 = /*#__PURE__*/ createElementVNode(...)
  • 静态节点 不参与 diff
  • 直接复用

Block Tree(块级优化)

Vue3 会把 动态节点 收集成一个 block:

JavaScript 复制代码
openBlock()
createElementBlock(...)

diff 只遍历 动态子节点。

相关推荐
吃杠碰小鸡3 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone3 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
Serene_Dream4 小时前
JVM 并发 GC - 三色标记
jvm·面试
xjt_09014 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农4 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king4 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳4 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵5 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星5 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_5 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js