Vue Diff算法核心是双端对比+key驱动的O(n)列表更新:通过sameVnode判断复用前提,patchVnode处理单节点更新,updateChildren用四匹配模式+查找机制高效处理子节点增删移动。

Vue.js 的 Diff 算法(即 patch 过程)核心在于用最少的 DOM 操作实现新旧虚拟节点(VNode)的高效比对与更新。它不是通用的最长公共子序列(LCS)算法,而是基于真实渲染场景做的深度优化:假设**同一层级的节点类型变化较少、列表中元素的顺序相对稳定、key 具有唯一性且合理使用**。以下聚焦 Vue 2.7(与 Vue 3 响应式分离,但 patch 逻辑更典型)源码(src/core/vdom/patch.js 中的 patchVnode 和 updateChildren),逐行拆解关键逻辑,并附流程图级说明。
一、patchVnode:单节点更新的主干逻辑
当新旧 VNode 类型相同时(如都是 div、或都是组件),进入 patchVnode —— 它决定“如何复用节点”,而非重建:
-
属性更新:调用
updateAttrs、updateClass、updateDOMListeners等钩子,仅 diff 并更新变更的属性/事件,不全量重写el.setAttribute; -
文本节点快捷路径:若新旧 vnode 均为文本节点(
isText),直接更新el.textContent,跳过子节点比对; -
子节点递归 patch:若新旧 vnode 都有子节点(
children),且类型一致(非文本),则进入updateChildren—— 这是 Diff 最复杂部分; - 子节点替换逻辑:若旧 vnode 有子节点而新 vnode 没有,则清空 DOM 子元素;若新 vnode 有子节点而旧 vnode 没有,则直接挂载;
二、updateChildren:双端对比 + key 驱动的列表 Diff
这是 Vue Diff 的灵魂所在,采用双指针 + 四种预设匹配模式,时间复杂度接近 O(n),避免 O(n²) 的暴力遍历。核心结构是维护四个索引:oldStartIdx / oldEndIdx(旧节点首尾)、newStartIdx / newEndIdx(新节点首尾),初始指向数组两端。
循环条件:oldStartIdx 。每轮尝试四种匹配(按顺序):
-
1. 头头匹配(sameVnode(oldStartVnode, newStartVnode)):若相同,递归
patchVnode更新该节点,然后oldStartIdx++、newStartIdx++; -
2. 尾尾匹配(sameVnode(oldEndVnode, newEndVnode)):相同则 patch,
oldEndIdx--、newEndIdx--; -
3. 头尾匹配(sameVnode(oldStartVnode, newEndVnode)):说明新节点末尾元素在旧节点开头,需将对应 DOM 节点移动到末尾(
elm.insertBefore(newEndVnode.elm, oldEndVnode.elm.nextSibling)),然后oldStartIdx++、newEndIdx--; -
4. 尾头匹配(sameVnode(oldEndVnode, newStartVnode)):说明新节点开头元素在旧节点末尾,移动 DOM 到开头(
elm.insertBefore(newStartVnode.elm, oldStartVnode.elm)),然后oldEndIdx--、newStartIdx++;
若以上四者都不匹配,则进入查找模式:用 newStartVnode.key 在旧节点剩余区间([oldStartIdx, oldEndIdx])中查找匹配项:
- 找到则复用该 DOM(
patchVnode),并将其真实 DOM 移动到oldStartVnode.elm之前; - 未找到则创建新节点(
createElm)插入到oldStartVnode.elm之前; - 无论是否找到,
newStartIdx++推进。
循环结束后,处理剩余节点:
Element Plus Vue3 桌面组件库。在开发 Vue 3.x 页面时调用,使用 Element Plus 组件(如 el-button、el-input、el-form、el-table 等)。
- 若
oldStartIdx > oldEndIdx:说明旧节点已耗尽,新节点还有剩余 → 批量createElm插入到oldEndVnode.elm.nextSibling后; - 若
newStartIdx > newEndIdx:说明新节点已耗尽,旧节点还有剩余 → 批量removeVnodes卸载对应 DOM。
三、sameVnode 判断:Diff 的前提基石
是否进入 patchVnode 的关键闸门,定义在 src/core/vdom/vnode.js:
return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) )
其中 key 是强制要求(无 key 时默认为 undefined,导致所有节点 key 相同,退化为低效的“就地复用”);tag 区分元素类型(div vs span);isComment 标识注释节点;data 存在性保证属性/事件等结构一致;sameInputType 特殊处理 input 类型(如 text vs checkbox 切换需重建)。没有 key 或 key 冲突,会极大降低 Diff 准确性。
四、流程图级执行路径(文字描述)
整个 Diff 流程可抽象为一棵决策树:
- 入口:
patch(oldVnode, newVnode)→ 判断是否 sameVnode?否 →removeVnode(oldVnode)+createElm(newVnode);是 → 进入patchVnode; -
patchVnode→ 是否文本节点?是 → 更新 textContent;否 → 是否有 children?都无 → 结束;一有一无 → 清空或挂载;都有 → 进入updateChildren; -
updateChildren→ 双端四匹配循环 → 匹配成功则 patch + 移动指针;失败则 key 查找 → 找到则 patch + 移动 DOM;未找到则 create → 指针推进;循环结束 → 处理剩余节点(create 或 remove)。
该设计本质是用空间换时间:通过 key 建立映射关系,将“查找”从 O(n) 降到 O(1)(哈希表),再配合双端策略覆盖常见移动场景(如 reverse、unshift、push),极少触发全量查找。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










