patch 打补丁流程深度解析
patch 是 Vue3 DOM 更新的总入口,对比新旧 VNode 找最小差异并精确应用到真实 DOM。 核心是同类型节点递归修补、不同类型节点直接替换,配合编译期 PatchFlags、双端 Diff 与 key 快速查找把算法压到 O(n)。
一句话概括
patch 是 Vue 3 DOM 更新的总入口——对比新旧 VNode,找到最小差异并精确应用到真实 DOM。核心逻辑一句话:同类型节点递归修补,不同类型节点直接替换。加上编译期 PatchFlags + 双端 Diff + key 快速查找,整个算法被压缩到了 O(n) 量级。
核心知识点
1. patch 的顶层逻辑——四步决策
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function patch(oldVNode, newVNode, container) {
// ① 类型不同 → 整个换
if (oldVNode.type !== newVNode.type) {
const newEl = createElement(newVNode);
oldVNode.el.parentNode.replaceChild(newEl, oldVNode.el);
return newEl;
}
// ② 类型相同 → 复用 DOM 节点
const el = (newVNode.el = oldVNode.el);
// ③ 更新 props(class、style、attrs、events)
patchProps(el, oldVNode.props, newVNode.props);
// ④ 更新 children —— 真正复杂的地方
patchChildren(oldVNode, newVNode, el);
return el;
}
2. patchChildren 的九种组合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function patchChildren(n1, n2, container) {
const c1 = n1.children, c2 = n2.children;
// 新子节点是文本
if (typeof c2 === 'string') {
if (Array.isArray(c1)) unmountChildren(c1); // 先卸载旧数组
if (c2 !== c1) container.textContent = c2;
}
// 新子是数组,旧子是文本
else if (Array.isArray(c2) && typeof c1 === 'string') {
container.textContent = '';
c2.forEach(c => mount(c, container));
}
// 新旧都是数组 → 核心 Diff 算法
else if (Array.isArray(c2) && Array.isArray(c1)) {
patchKeyedChildren(c1, c2, container); // 面试的绝对重点
}
// 新子是 null(清空)
else if (!c2 && c1) {
unmountChildren(c1);
}
}
3. 双端 Diff——四个方向同时推进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function patchKeyedChildren(c1, c2, container) {
let i = 0; // 头指针
let e1 = c1.length - 1, e2 = c2.length - 1; // 尾指针
// 第一阶段:四方向快速匹配
while (i <= e1 && i <= e2) {
// ① 头对头 —— 最常见(从前面删/加)
if (sameVNode(c1[i], c2[i])) { patch(c1[i], c2[i]); i++; }
// ② 尾对尾 —— 次常见(从后面删/加)
else if (sameVNode(c1[e1], c2[e2])) { patch(c1[e1], c2[e2]); e1--; e2--; }
// ③ 旧头 vs 新尾 —— 旧头移到最后
else if (sameVNode(c1[i], c2[e2])) {
container.insertBefore(c1[i].el, c1[e1].el.nextSibling);
patch(c1[i], c2[e2]); i++; e2--;
}
// ④ 旧尾 vs 新头 —— 旧尾移到最前
else if (sameVNode(c1[e1], c2[i])) {
container.insertBefore(c1[e1].el, c1[i].el);
patch(c1[e1], c2[i]); e1--; i++;
}
// ⑤ 四处不中 → 用 key 建立映射表 → O(1) 查找
else break;
}
// 第二阶段:处理剩余
if (i > e1 && i <= e2) {
// 旧的全匹配完了,新的还有剩余 → 批量新增
for (let j = i; j <= e2; j++) mount(c2[j], container);
} else if (i > e2 && i <= e1) {
// 新的全匹配完了,旧的还有剩余 → 批量删除
for (let j = i; j <= e1; j++) unmount(c1[j]);
}
// 第三阶段(else):中间乱序部分 → key 映射表查找 + 最长递增子序列
}
4. PatchFlags——编译期的降维打击
1
2
3
4
5
6
7
8
9
10
11
// 模板编译时 Vue 编译器生成:
// <div :class="cls" :id="id">静态文本</div>
// ↓ 编译结果 ↓
const vnode = h('div', { class: cls, id: id }, '静态文本');
vnode.patchFlag = 1 /* CLASS */ | 4 /* PROPS */ | 64 /* HOISTED */;
// patch 时:只检查 patchFlag 标记过的属性
// 静态文本直接跳过比较——位运算 O(1) 判断
if (vnode.patchFlag & PatchFlags.CLASS) updateClass(el, newProps.class);
if (vnode.patchFlag & PatchFlags.STYLE) updateStyle(el, newProps.style);
if (vnode.patchFlag & PatchFlags.PROPS) updateProps(el, newProps);
这就是 Vue 3 比 Vue 2 diff 快的关键: 编译期静态分析打标 → 运行时只 diff 真正动态的部分 → 大量静态节点零开销跳过。
5. patchProps——属性更新的精细控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function patchProps(el, oldProps = {}, newProps = {}) {
// 先删旧属性(新 props 里没有的)
for (const key in oldProps) {
if (!(key in newProps)) {
if (key.startsWith('on')) el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
else el.removeAttribute(key);
}
}
// 再设新属性(值变了的)
for (const key in newProps) {
const old = oldProps[key], val = newProps[key];
if (old === val) continue;
if (key === 'class') el.className = val; // class 直接走 className
else if (key === 'style') patchStyle(el, old, val); // style 单独处理
else if (key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase(), old);
el.addEventListener(key.slice(2).toLowerCase(), val);
} else el.setAttribute(key, val);
}
}
其实你每天都在用
- v-for 拖拽排序: 只移动一个 DOM 节点——头尾匹配 + key 映射 → O(1) 定位 + O(n) 移动
- v-if/v-else 切换: tag 不同 → 走替换分支,不递归内容——快速卸载 + 重建
- :class 动态绑定:
patchFlag & CLASS→ 只更新el.className,跳过所有其他 props - v-model 输入: 每按键 → set 触发 → patch textContent,只改一个文本节点
- Transition 动画: VNode 标记
leaving后 patch 延迟卸载——DOM 保留到动画跑完
常见误解(FAQ)
❌ 误区:「Vue 的 Diff 和 React 的全量比对一个级别」 Vue 3 多了一层编译期优化——PatchFlags + block tree 让大量静态节点被标记跳过,diff 步数远少于同等规模 React 的全量 reconciliation。
❌ 误区:「双端 diff 在任何情况下都是 O(n)」 双端 diff 在最佳情况(头尾变化)确实是 O(n)。但中间乱序部分需要建立 key→index 映射表 + 最长递增子序列,这部分是 O(n log n)。只是整体仍然比没有编译优化的 O(n²) 快得多。
❌ 误区:「没有 key 也能正确 diff」 能跑但能跑错——没有 key 时节点复用判断退化为 tag 浅比较,reactivity 的绑定会被”就地复用”导致 input 内容串位。
key不是性能优化,是正确性保证。❌ 误区:「patchProps 的 class 属性直接 setAttribute 就行」 Vue 把 class 和 style 单独拎出来处理——
el.className直接赋值比setAttribute('class', ...)快,style 的 patch 更是只改变化的 CSS 属性,不动整体。这些都是 60fps 的微观保证。
一句话总结
patch 不复杂——同类型修修补补、不同类型推倒重建。Vue 3 通过编译期 PatchFlags 标记 + 双端 Diff + key 映射,把这句话实现到了 O(n) 量级的极致。