Vue 中,组件的渲染会生成虚拟 DOM(VNode,一个描述真实 DOM 的 JavaScript 对象)。当数据变化时,会生成新的 VNode 树,diff 算法的作用就是对比新旧 VNode 树,找到差异(“补丁”),并通过 patch 方法将差异更新到真实 DOM。
Vue3 的 diff 算法主要针对 子节点列表(children) 的对比(因为大多数 DOM 变化发生在列表中),核心思路是 “精准定位差异,减少不必要的对比和 DOM 操作”。
Vue3 在 编译阶段 会对模板进行分析,给 VNode 打上 静态标记(PatchFlag),标识节点是否为静态(内容不会变化),或动态部分的类型(如文本、属性、样式等)。
- 静态节点(如纯文本
<div>静态内容</div>)会被标记为 PatchFlags.NEED_PATCH 以外的类型,diff 时直接跳过对比(因为它们永远不会变化)。
- 动态节点仅对比标记中指定的动态部分(如只对比文本内容,忽略其他静态属性)。
示例:模板 <div :id="id">Hello {{ name }}</div> 编译后,VNode 的 patchFlag 会标记为 “需要更新文本和 id 属性”,diff 时只检查这两处,无需全量对比节点的所有属性。
当新旧 VNode 都包含子节点(children 为数组)时,Vue3 会分步骤对比,核心逻辑在 patchChildren 方法中。
通过 4 个指针 分别指向新旧子节点列表的首尾,快速对比首尾节点是否可复用(即 key 和 type 相同),减少中间节点的对比次数:
oldStartIdx:旧列表的起始索引
oldEndIdx:旧列表的结束索引
newStartIdx:新列表的起始索引
newEndIdx:新列表的结束索引
对比规则:
- 若
旧首节点 与 新首节点 可复用:直接 patch 该节点,双指针同时后移。
- 若
旧尾节点 与 新尾节点 可复用:直接 patch 该节点,双指针同时前移。
- 若
旧首节点 与 新尾节点 可复用:patch 后将旧首节点移动到旧尾节点后面,旧首指针后移,新尾指针前移。
- 若
旧尾节点 与 新首节点 可复用:patch 后将旧尾节点移动到旧首节点前面,旧尾指针前移,新首指针后移。
通过首尾对比,可快速复用大量连续的、未移动的节点,减少后续复杂对比的范围。
当首尾对比无法匹配时,剩余节点需要通过 key 查找可复用的节点(Vue 要求列表渲染时加 key,就是为了这里的快速匹配):
- 用一个
keyToOldIdx 映射表存储旧节点的 key 与索引({ key: 索引 }),避免遍历查找,提升效率。
- 遍历新列表中剩余的节点,通过
key 在 keyToOldIdx 中查找对应的旧节点:
- 若找到且类型匹配:patch 该节点,并标记为 “已处理”。
- 若未找到:说明是新节点,直接创建并插入。
经过步骤 1 和 2 后,需要处理剩余的未匹配旧节点(需删除)和已匹配节点的位置调整(需移动)。为了减少 DOM 移动操作(DOM 移动性能成本高),Vue3 引入 最长递增子序列(LIS) 算法:
- 收集已匹配节点在旧列表中的索引,形成一个 “旧索引序列”。
- 计算该序列的 最长递增子序列(即无需移动的节点,因为它们在新列表中的相对顺序与旧列表一致)。
- 剩余节点根据 LIS 的位置反向插入,只需移动最少次数即可完成排序。
举例:旧节点索引序列 [2, 1, 3, 0],其 LIS 为 [1, 3](或 [2, 3]),这两个节点无需移动,其他节点围绕它们插入,减少移动次数。
最后,删除旧列表中未被匹配的节点(已无用),完成 diff 过程。

Vue3 的 diff 算法通过 编译时静态标记 减少不必要的对比,通过 首尾快速匹配 和 key 映射表 提升查找效率,最终通过 最长递增子序列 减少 DOM 移动操作,实现了比 Vue2 更高效的虚拟 DOM 更新。这些优化使得 Vue3 在大型列表和复杂组件的渲染性能上有显著提升。