《Vue3 从入门到大神26篇》Virtual DOM 与 Diff 算法 —— Vue3 的高效更新机制

《Vue3 从入门到大神26篇》Virtual DOM 与 Diff 算法 —— Vue3 的高效更新机制

前言

这一篇我们来了解vue3高效更新机制的核心逻辑,很多人对 Virtual DOM(虚拟 DOM)有一个误解:

❌ “Virtual DOM 比原生 DOM 快。”

这是一个伪命题

事实上,操作 Virtual DOM 本身也有成本,它之所以能提升性能,是因为:

它能计算出最小更新量,从而减少昂贵的 DOM 操作。

而 Vue3 的 Diff 算法,正是为了尽可能少地操作 DOM

这一篇,我们系统讲解:

  • ✅ 什么是 Virtual DOM

  • ✅ VNode 的设计

  • ✅ Vue3 的 Diff 策略

  • ✅ 最长递增子序列(LIS)在 Diff 中的作用

  • ✅ 为什么 Vue3 的 Diff 更快


一、为什么需要 Virtual DOM?

1️⃣ 原生 DOM 的问题

document.getElementById('app').innerHTML = ` <div> <p>${count}</p> <button>Click</button> </div> `

❌ 问题:

  • DOM 操作非常昂贵(重排、重绘)

  • 每次更新都重新创建所有 DOM

  • 无法复用已有 DOM


2️⃣ Virtual DOM 的核心思想

数据变化 ↓ 生成新的 Virtual DOM(JS 对象) ↓ Diff 对比新旧 VDOM ↓ 计算出最小变更 ↓ 只更新必要的真实 DOM

📌Virtual DOM 是“中间层”,不是“性能银弹”。


二、VNode:Virtual DOM 的基本单元

1️⃣ VNode 是什么?

interface VNode { type: string | object | null // 标签名 / 组件对象 props: Record<string, any> | null children: VNode[] | string | null el: HTMLElement | null // 对应的真实 DOM key?: string | number }

2️⃣ 一个真实 VNode 示例

const vnode = { type: 'div', props: { class: 'container' }, children: [ { type: 'p', props: null, children: 'Hello Vue3' } ], el: null }

3️⃣ h 函数:创建 VNode 的工具

function h(type, props, children) { return { type, props, children, el: null } } // 使用 h('div', { class: 'container' }, [ h('p', null, 'Hello Vue3') ])

📌Vue 模板编译后,最终都会变成h()调用。


三、Diff 算法:找出差异

1️⃣ Diff 的本质

给定两棵树(旧 VDOM 和新 VDOM),找出最小的修改操作。

这是一个经典的树编辑距离问题,理论上时间复杂度是O(n³)

Vue 做了一个重要假设:

只比较同一层级的节点,不跨层级比较。

这样时间复杂度降到了O(n)


2️⃣ Vue2 的双端 Diff

Vue2 使用的是双端比较算法

旧:[a, b, c, d] 新:[b, c, e, d]

比较流程:

  • 旧头 vs 新头

  • 旧尾 vs 新尾

  • 旧头 vs 新尾

  • 旧尾 vs 新头

✅ 优点:逻辑直观

❌ 缺点:移动节点时不够高效


四、Vue3 的 Diff:更聪明的策略

Vue3 在双端比较的基础上,引入了最长递增子序列(LIS)优化。

1️⃣ 核心思路

1. 预处理:处理相同的前缀和后缀 2. 构建 key -> index 映射表 3. 找出最长不需要移动的节点序列 4. 只移动必要的节点

2️⃣ 示例:为什么要 LIS?

<!-- 旧 VDOM --> <ul> <li key="a">A</li> <li key="b">B</li> <li key="c">C</li> <li key="d">D</li> </ul> <!-- 新 VDOM --> <ul> <li key="b">B</li> <li key="c">C</li> <li key="e">E</li> <li key="d">D</li> </ul>

变化:

  • A 被删除

  • E 被插入

  • B、C、D 顺序变化


3️⃣ 最长递增子序列的作用

Vue3 会找出在新旧 VDOM 中相对位置不变的节点序列

旧索引: [0, 1, 2, 3] (a, b, c, d) 新索引: [1, 2, 3, 0] (b, c, d, a) 最长递增子序列: [1, 2, 3] → 对应 b, c, d

📌结论

b、c、d 这三个节点不需要移动,只需要移动其他节点。


五、LIS 算法简化实现

function getLIS(arr) { const dp = new Array(arr.length).fill(1) let maxLen = 1 for (let i = 1; i < arr.length; i++) { for (let j = 0; j < i; j++) { if (arr[i] > arr[j]) { dp[i] = Math.max(dp[i], dp[j] + 1) } } maxLen = Math.max(maxLen, dp[i]) } // 回溯构造 LIS const lis = [] for (let i = arr.length - 1; i >= 0; i--) { if (dp[i] === maxLen) { lis.unshift(i) maxLen-- } } return lis } // 示例 console.log(getLIS([1, 2, 3, 0])) // [0, 1, 2]

📌Vue3 使用的是 O(n log n) 的贪心算法版本,这里为了直观用了 O(n²)。


六、Vue3 Diff 的核心优势

对比项

Vue2

Vue3

比较策略

双端比较

双端 + LIS

移动节点

频繁移动

最小化移动

性能

较好

更好

复杂度

O(n)

O(n),常数更小

📌Vue3 的 Diff 不是"更快",而是"更少移动 DOM"。


七、PatchFlags 与 Diff 的关系

还记得第 22 篇讲的 PatchFlags 吗?

// 编译后的渲染函数 _createElementVNode("p", null, _toDisplayString(_ctx.count), 1 /* TEXT */)

📌PatchFlags 是 Diff 的上游优化

  • PatchFlags 告诉 Diff:这个节点只有文本变化

  • Diff 就只更新文本内容,不再递归比较子节点

两者结合,效果翻倍。


八、手写一个简化的 Diff(理解版)

function diff(oldChildren, newChildren, container) { const oldKeyToIndex = {} oldChildren.forEach((child, i) => { oldKeyToIndex[child.key] = i }) const newIndices = new Array(newChildren.length).fill(-1) // 建立新节点到旧节点的映射 newChildren.forEach((child, i) => { const oldIndex = oldKeyToIndex[child.key] if (oldIndex !== undefined) { newIndices[i] = oldIndex } }) // 计算 LIS const lis = getLIS(newIndices) // 逆序遍历,移动节点 let j = lis.length - 1 for (let i = newChildren.length - 1; i >= 0; i--) { const newChild = newChildren[i] const oldIndex = newIndices[i] if (oldIndex === -1) { // 新增节点 mount(newChild, container) } else { if (i !== lis[j]) { // 需要移动 move(newChild, container) } else { // 不需要移动 j-- } } } }

九、面试高频问答

Q1:Virtual DOM 一定比原生 DOM 快吗?

不一定。VDOM 的优势在于减少 DOM 操作次数,而不是操作本身更快。

Q2:Vue3 的 Diff 和 Vue2 有什么不同?

Vue3 在双端比较基础上引入 LIS,最小化节点移动次数。

Q3:LIS 在 Diff 中的作用是什么?

找出不需要移动的节点序列,只移动必要的节点。


十、总结(原理级)

  • Virtual DOM 是描述 UI 的 JS 对象

  • Diff 的目标是最小化 DOM 操作

  • Vue3 的 Diff = 双端比较 + LIS

  • PatchFlags 是 Diff 的上游优化

  • 少移动 DOM,才是真正的快


📢 下期预告

👉第 27 篇:编译时优化揭秘 —— Vue 模板是如何变成 JS 代码的?