《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 代码的?