patch 打补丁流程
patch 打补丁流程
一句话概括
patch 是 Vue 3 的 DOM 更新入口函数——它对比新旧 VNode,找到最小差异集并精准应用到真实 DOM。核心逻辑就一句:”同类型节点递归 patch,不同类型节点直接替换”。
核心知识点
1. patch 函数的整体流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function patch(oldVNode, newVNode) {
// 1. 类型不同 → 整个替换
if (oldVNode.tag !== newVNode.tag) {
return oldVNode.el.replaceWith(createEl(newVNode));
}
// 2. 同类型 → 复用 DOM 节点,更新属性
const el = (newVNode.el = oldVNode.el);
// 3. 更新 props
patchProps(el, oldVNode.props, newVNode.props);
// 4. 更新 children(核心复杂度在此)
patchChildren(oldVNode, newVNode, 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
23
function patchChildren(oldVNode, newVNode, el) {
const oldCh = oldVNode.children;
const newCh = newVNode.children;
// 情况 1:新节点是文本
if (typeof newCh === 'string') {
if (Array.isArray(oldCh)) unmountChildren(oldCh); // 先卸载旧的一组子节点
if (newCh !== oldCh) el.textContent = newCh;
return;
}
// 情况 2:新节点是数组,旧节点是文本
if (Array.isArray(newCh) && typeof oldCh === 'string') {
el.textContent = '';
newCh.forEach(child => el.appendChild(createEl(child)));
return;
}
// 情况 3:新节点是数组,旧节点也是数组 → 核心 Diff
if (Array.isArray(newCh) && Array.isArray(oldCh)) {
diffArray(oldCh, newCh, el);
}
}
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
35
36
37
38
39
40
41
42
43
44
45
46
function diffArray(oldCh, newCh, el) {
let oldStart = 0, newStart = 0;
let oldEnd = oldCh.length - 1, newEnd = newCh.length - 1;
while (oldStart <= oldEnd && newStart <= newEnd) {
// 头对头
if (sameVNode(oldCh[oldStart], newCh[newStart])) {
patch(oldCh[oldStart++], newCh[newStart++]);
}
// 尾对尾
else if (sameVNode(oldCh[oldEnd], newCh[newEnd])) {
patch(oldCh[oldEnd--], newCh[newEnd--]);
}
// 头对尾(旧的头移到旧的尾)
else if (sameVNode(oldCh[oldStart], newCh[newEnd])) {
el.insertBefore(oldCh[oldStart].el, oldCh[oldEnd].el.nextSibling);
patch(oldCh[oldStart++], newCh[newEnd--]);
}
// 尾对头(旧的尾移到旧的头)
else if (sameVNode(oldCh[oldEnd], newCh[newStart])) {
el.insertBefore(oldCh[oldEnd].el, oldCh[oldStart].el);
patch(oldCh[oldEnd--], newCh[newStart++]);
}
// 四个位置都对不上 → 用 key 在旧节点中查找
else {
const idx = findIdxByKey(oldCh, newCh[newStart].props?.key);
if (idx >= 0) {
el.insertBefore(oldCh[idx].el, oldCh[oldStart].el);
patch(oldCh[idx], newCh[newStart]);
oldCh[idx] = null; // 标记已处理
} else {
el.insertBefore(createEl(newCh[newStart]), oldCh[oldStart].el);
}
newStart++;
}
}
// 收尾:新增 / 删除
if (newStart <= newEnd) {
// newCh 剩余 → 批量新增
for (let i = newStart; i <= newEnd; i++) el.appendChild(createEl(newCh[i]));
} else if (oldStart <= oldEnd) {
// oldCh 剩余 → 批量删除
for (let i = oldStart; i <= oldEnd; i++) oldCh[i]?.el.remove();
}
}
4. patchProps——属性更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function patchProps(el, oldProps = {}, newProps = {}) {
// 移除旧属性
for (const key in oldProps) {
if (!(key in newProps)) el.removeAttribute(key);
}
// 更新/新增属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newProps[key]);
} else {
el.setAttribute(key, newProps[key]);
}
}
}
}
5. Vue 3 Diff 的编译期优化——PatchFlags
1
2
3
4
5
6
7
8
9
10
11
12
// 编译时,Vue 模板编译器会给动态内容打上 PatchFlags
// <div :class="cls" :id="id">hello</div>
// 编译结果:
const vnode = h('div', { class: cls, id: id }, 'hello');
vnode.patchFlag = 1 /* CLASS */ | 2 /* STYLE */ | 4 /* PROPS */;
// patch 时:只检查 patchFlag 标记的属性类型
function patchElement(old, new) {
if (new.patchFlag & 1) patchClass(el, old.class, new.class);
if (new.patchFlag & 4) patchProps(el, old.props, new.props);
// 静态内容:跳过比较
}
其实你每天都在用
- v-for 列表重排:拖拽排序一条,diff 只移动一个 DOM 节点,其余原地不动
- v-if / v-else 切换:不同 tag 直接 replace,不需要深挖内容
- :class 动态绑定:只有 class 变了,只 patch class 属性,跳过其他 props
- v-model 输入:每次按键触发 set → patch textContent,只改一个文本节点
- Transition 组件:VNode 离开时打上 leaving 标记,patch 延迟执行卸载直到动画完成
常见误解
❌ 误区:「Diff 就是全量比对,React/Vue 都一样」 Vue 3 比 React 多了一层编译期优化——静态标记和 block tree 让大量静态内容直接跳过 diff。React 没有编译期,靠 Fiber 用时间切片分摊 diff 开销。
❌ 误区:「双端 Diff 比简单 Diff 更快」 仅在”列表首尾变化”场景更快(如前面插入一条)。对于随机位置的大规模变化,最终会退化为 O(n²) 的查找。key 的存在可以让查找降到 O(1)。
❌ 误区:「没有 key 时 Vue 用的也是双端 diff」 没有 key 时仍然走双端 diff,但”相同节点判断”退化为 tag + 属性的浅比较。最大的问题是就地复用——input 内容会串位,因为节点被复用而非移动。所以
:key不是改性能,是改正确性。❌ 误区:「patchChildren 中三个分支的优先级是固定的」 是的,但 Vue 3 内部实际上用了更复杂的位运算 flags 来区分 16 种 children 形态(文本、数组、空、片段等),不是简单的 if-else,而是编译期生成的 shapeFlag + patchFlag 联合判断。
一句话总结
patch 不大,就是”同类型修修补补、不同类型推倒重建”——Vue 的编译优化 + 双端 diff + key 复用,把这句话做到了 60fps 的极致。
本文由作者按照 CC BY 4.0 进行授权