O(n^3)
降到 O(n)
?在浏览器中,直接操作真实 DOM 会导致:
虚拟 DOM 是为了解决上述问题:通过使用 JavaScript 对象表示 DOM 结构,在内存中对比虚拟 DOM 的变化,再以最小的代价更新真实 DOM。
比较两棵树的最小差异(最优编辑距离)的复杂度是 O(n^3):
优化:
O(n^3)
降到接近 O(n)
。Vue 的 Diff 算法基于以下假设进行优化:
key
的重要性:key
可以唯一标识节点,帮助算法直接找到可复用的节点,避免大量顺序比较。基于这些假设,Vue 的 Diff 算法在实现中采用了 双端比较 和 局部优化 的策略,大幅提升了性能。
Vue 的 Diff 算法分为两部分:
key
快速定位节点,处理新增、删除、复用和移动的场景。// 旧虚拟 DOM
const oldVNode = { tag: 'div', props: { id: 'a' }, children: ['Hello'] }
// 新虚拟 DOM
const newVNode = { tag: 'div', props: { id: 'b' }, children: ['World'] }
// 结果
// 1. 更新属性 id:从 'a' 改为 'b'
// 2. 更新子节点内容:从 'Hello' 改为 'World'
O(n)
。O(n)
。Vue 使用 双端比较 处理同层的子节点,主要逻辑如下:
旧子节点:
oldChildren = [a, b, c, d]
新子节点:
newChildren = [b, c, e, a]
比较过程:
a
≠ b
,指针不动。d
≠ a
,指针不动。b
和 c
可复用。e
为新增节点。d
,移动 a
。最终结果:
d
,新增 e
,移动 a
。双端比较避免了全量扫描,节点的处理复杂度为 O(n)
。
key
的优化当子节点带有唯一的 key
时,Vue 可通过哈希表快速定位节点,进一步优化性能:
key
相同,则直接复用该节点。key
,算法会退化为简单的顺序比较。示例:带 key
的 Diff
oldChildren = [{ key: 1, tag: 'div' }, { key: 2, tag: 'span' }]
newChildren = [{ key: 2, tag: 'span' }, { key: 1, tag: 'div' }]
结果:通过 key
快速匹配 div
和 span
,避免不必要的比较。
Vue 3 在 Diff 算法中引入了以下性能优化策略:
Vue 3 的编译器会为模板中的节点生成 Patch Flag,用于标记动态内容。例如:
{{ staticText }}
{{ dynamicText }}
编译后:
staticText
被直接跳过。dynamicClass
和 dynamicText
被标记为动态,更新时只处理这些部分。Vue 3 的虚拟 DOM 被设计为 Block Tree,静态节点和动态节点被分组处理:
以下是 Vue 3 中 patchChildren
函数的核心实现(简化版):
function patchChildren(c1, c2, container) {
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 1. 头部比较
while (i <= e1 && i <= e2) {
if (c1[i].key === c2[i].key) {
patch(c1[i], c2[i], container)
i++
} else {
break
}
}
// 2. 尾部比较
while (e1 >= i && e2 >= i) {
if (c1[e1].key === c2[e2].key) {
patch(c1[e1], c2[e2], container)
e1--
e2--
} else {
break
}
}
// 3. 中间部分处理
if (i > e1) {
// 新增节点
} else if (i > e2) {
// 删除节点
} else {
// 处理可复用节点
}
}
Vue 的核心渲染流程是通过虚拟 DOM 计算视图的变化,然后将差异反映到真实 DOM 中。
示例:动态列表渲染
- {{ item.text }}
初始数据:
items = [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]
用户操作导致数据更新为:
items = [
{ id: 3, text: 'Item 3' },
{ id: 2, text: 'Item 2' },
{ id: 4, text: 'Item 4' }
]
id=3
和 id=2
被识别为可复用节点。id=4
是新增节点。id=1
被标记为需要删除。最终 Vue 会按照最小变更路径更新真实 DOM,而不是重建整个列表。
Vue 的组件系统依赖于 Diff 算法判断子组件的复用与销毁。在以下场景中,Diff 算法会决定是否复用现有组件实例。
示例:动态切换组件
切换 currentComponent
时:
key
不同,则销毁旧组件,重新创建新组件。key
相同,则复用组件实例,仅更新其属性或插槽内容。例如:
/profile
和 /settings
页面,这两个页面都基于同一个 UserInfo
组件,只是 props
不同。UserInfo
实例,仅更新其属性,而不会销毁并重建。key
不同时(例如按路由 ID 动态切换用户详情页面),Vue 会销毁旧组件并重新挂载新组件,以保证状态隔离。虽然 Diff 算法主要比较同一层级的节点,但某些情况下会涉及到跨层级的更新,例如子树发生结构变化。
示例:条件渲染
Condition A
Condition B
当 show
从 true
切换为 false
时,Vue 会销毁 Condition A
的节点,并挂载 Condition B
的新节点。
优化点:
key
提示 Vue 跳过不必要的复杂 Diff。Vue 的 Diff 算法通过一系列优化(如双端比较、静态标记、Block Tree)将复杂度降低到接近 O(n)
,并结合实际场景进一步优化性能。
核心思路是去除头尾重复的节点。其次便是采用了最长递增子序列来复用相对位置没有发生变化的节点,这些节点是不需要移动的,便能最快的复用和更新。