Vue3 的 DOM Diff 算法
为什么需要 Diff
Vue 的响应式更新粒度是组件级别。当组件内部某个响应式数据变化时,整个组件的 render 函数重新执行,生成一棵新的虚拟 DOM 树。如果直接将整棵虚拟 DOM 树映射为真实 DOM 操作并应用到页面上,会产生大量不必要的 DOM 更新——实际可能只有一两个节点发生了实质性变化。
虚拟 DOM 的作用是提供一个"中间层",让框架有机会在内存中对比新旧两棵虚拟 DOM 树,找出真正需要更新的部分。Diff 算法就是完成这个对比的核心逻辑。
Vue3 Diff 的整体策略
Vue3 的 Diff 策略由三个层次组成:同层比较、双端指针、最长递增子序列。整体复杂度为 O(n)。
同层比较:只对同一层级的节点进行对比,不跨层级。这是所有基于虚拟 DOM 的框架共同遵循的前提——跨层级的节点变动极其罕见,为它付出额外复杂度不划算。
双端指针:在新旧两组子节点上各维护两个指针,分别指向头部和尾部,循环向内收缩。每一步比较四种情况:旧头 vs 新头、旧尾 vs 新尾、旧头 vs 新尾、旧尾 vs 新头。这种方式在处理列表的反转和移位时效率很高,大部分常见操作在首轮就能匹配。
最长递增子序列(LIS):当双端比较不足以匹配所有节点时,进入中间乱序区间的处理。Vue3 计算新节点在旧节点中的位置序列,对这个序列求最长递增子序列。这个子序列中的节点位置相对顺序是正确的,无需移动;其余节点按新的顺序插入或移位。这是 Vue3 相较于 Vue2 在同层比较层面的主要升级。
与 Vue2 Diff 的对比
Vue2 的 Diff 同样是同层 + 双端指针,但在两个方面不如 Vue3:
编译阶段优化:Vue2 的 Diff 发生在纯运行时,每次更新都要遍历整棵虚拟 DOM 树。Vue3 在编译阶段就做了大量预处理:
- 静态提升:将模板中不会变化的纯静态节点从
render函数中提升到外层,每次render执行时直接复用同一个 VNode 引用,避免重复创建 - Patch Flag:在动态节点上标记"会变什么"——
1表示文本会变、2表示 class 会变、8表示 props 会变。运行时 Diff 时只需检查被标记的部分,跳过所有静态节点 - Block Tree:将模板拆分为静态节点树和动态节点树(Block Tree),Diff 时只遍历动态节点树,整段静态内容被跳过
最长递增子序列:Vue2 在中间乱序区间的处理依赖纯粹的遍历和位置标记,Vue3 的 LIS 算法可以更精确地确定哪些节点的位置已经正确,减少不必要的 DOM 移动操作。
与 React Diff 的对比
React 的 Diff 策略同样是同层比较,但对节点的判断方式不同:
- React 通过
type属性判断两个节点是否为同一类型——type不同则整棵子树销毁重建 - React 通过
key属性在列表中复用节点实例,没有双端指针或 LIS 优化 - React 的 Diff 与 Fiber 架构深度绑定,可中断、可分片执行,这是 React 并发模式的基石
Vue3 的 Diff 追求"用更少的 DOM 操作完成更新",React 的 Diff 追求"让 Diff 本身可以被打断"。两种设计取向的差异源于它们在不同架构层面对性能瓶颈的判断不同。
一个具体的例子
<template>
<div>
<h1>静态标题</h1>
<p :class="dynamicClass">动态文本:{{ message }}</p>
<span>静态尾部</span>
</div>
</template>编译后,<h1> 和 <span> 被识别为纯静态节点并被提升。<p> 被打上 Patch Flag,标记其 class 和文本内容可能变化。Diff 阶段,Vue3 只看 <p> 的变化,<h1> 和 <span> 直接跳过。如果中间有一长串列表项,双端指针先快速匹配头尾不变的项,LIS 再处理中间变化的部分——整个过程不需要遍历每一对兄弟节点。