patch方法详解
# 概览
patch
方法是vue3中虚拟DOM更新的核心方法,负责比较新旧虚拟节点(VNode
)并更新DOM
# 源码解析
# 参数说明
patch
的参数说明如下:
n1
:旧的VNode
,若为null
,则表示需要挂载n2
:新的VNode
container
:容器DOM元素anchor
:参考节点(插入新节点时使用)parentComponent
:父组件实例parentSuspense
:父级Suspense
实例namespace
:命名空间(用于SVG
等)slotScopeIds
:插槽作用域ID(用于优化)optimized
:是否开启优化模式(默认根据新节点是否有动态子节点决定)
# 源码分析
源码分析如下:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, namespace = void 0, slotScopeIds = null, optimized = !!n2.dynamicChildren) => {
if (n1 === n2) {
// 若新旧节点是同一个对象,则直接退出,无需更新
return;
}
// 比较节点类型
if (n1 && !isSameVNodeType(n1, n2)) {
// 若新旧节点类型不同,则
// 获取旧节点的下一个DOM节点作为锚点
anchor = getNextHostNode(n1);
// 卸载旧节点
unmount(n1, parentComponent, parentSuspense, true);
// 将n1置空,后续挂载n2,而不是更新
n1 = null;
}
// 判断n2的patchFlag
if (n2.patchFlag === -2) {
// 关闭优化模式,强制全量Diff
optimized = false;
n2.dynamicChild = null;
}
// 获取节点类型
const { type, ref, shapeFlag } = n2;
switch (type) {
// 文本节点
case Text:
processText(n1, n2, container, anchor);
break;
// 注释节点
case Comment:
processCommentNode(n1, n2, container, anchor);
// 静态节点
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, namespace);
};
break;
// Fragment 片段
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized);
break;
// 其他类型通过shapeFlag判断
default:
if (shapeFlag & 1) {
// Element 普通DOM元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopedIds, optimized)
} else if (shapeFlag & 6) {
// Component 组件 有状态
processComponent(n1, n2, container, anchor, parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized)
} else if (shapeFlag & 64) {
// Teleport 组件
type.process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals
)
} else if (shapeFlag & 128) {
// Suspense 组件
type.process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals
);;
}
}
// 处理 Ref引用
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
} else if (ref == null && n1 && n1.ref != null) {
setRef(n1.ref, null, parentSuspense, n1, true)
}
}
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 关键设计思想
1.双端Diff优化
- 通过
optimized
标记跳过静态子树比对 - 动态子节点
dynamicChildren
直接进行定向更新
2.PatchFlags优化
- 标志位控制Diff策略,减少不必要的操作
3.组件与DOM分离
- 组件通过
processComponent
处理生命周期/状态 - DOM元素通过
processElement
处理属性/子节点
4.特殊组件内置逻辑
Teleport/Suspense
直接调用其内部方法,实现跨容器/异步渲染
# 辅助方法
# processText
处理文本
processText
用于创建/更新文本节点。
processText
的源码如下:
const processText = (n1, n2, container, anchor) => {
if (n1 == null) {
// 旧节点 n1为null,则直接挂载n2
// hostCreateText创建文本节点,再调用hostInsert将文本节点插入到container容器中
hostInsert(n2.el = hostCreateText(n2.children), container, anchor)
} else {
const el = n2.el = n1.el;
// 比较n1和n2的子元素children,若二者不同,则调用hostSetText设置文本
if (n2.children !== n1.children) {
hostSetText(el, n2.children);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
hostInsert
、hostCreateText
与hostSetText
都是在runtime-dom
中实现的操作DOM元素的方法。
# processCommentNode
处理注释节点
processCommentNode
用于创建或者是更新注释节点。
processCommentNode
的源码实现如下:
const processCommentNode = (n1,n2,container,anchor)=>{
if(n1 == null){
// 若n1为null,则挂载n2的el
hostInsert(n2.el = hostCreateComment(n2.children || ""),container,anchor)
}else{
n2.el = n1.el;
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
同上,hostCreateComment
同样也是在runtime-dom
中实现的操作注释节点。
# mountStaticNode
挂载静态节点
mountStaticNode
用于挂载静态节点,当n1
为null
且n2
的节点类型为静态节点时,才会执行该方法。
const mountStaticNode = (n2, container, anchor, namespace) => {
[n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, namespace, n2.el, n2.anchor)
}
1
2
3
2
3
# processFragment
处理片段
processFragment
用于处理Fragment
片段节点的核心函数。
const processFragment = (n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => {
// 创建/复用锚点节点,若n1存在,则是更新,复用旧节点的锚点;否则是挂载,创建两个空文本节点标记起止位置
const fragmentStartAnchor = n2.el = n1 ? n1.el : hostCreateText("");
const fragmentEndAnchor = n2.anchor = n1 ? n1.anchor : hostCreateText("");
let { patchFlag, dynamicChildren, slotScopeIds, fragmentSlotScopeIds } = n2;
// 处理插槽作用域ID
if (fragmentSlotScopeIds) {
// 若Fragment自身有插槽作用域ID,则将其合并到slotScopeIds中,用于子节点更新优化
slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds;
}
// 若n1为null,则说明是挂载
if (n1 == null) {
// 通过hostInsert插入起止(空文本)节点
hostInsert(fragmentStartAnchor, container, anchor);
hostInsert(fragmentEndAnchor, container, anchor);
// 调用mountChildren挂载子节点
mountChildren(n2.children || [], container, fragmentEndAnchor, parentComponent, parentSuspense, namespace, slotScopedIds, optimized)
} else {
//更新操作
if (patchFlag > 0 && patchFlag & 64 && dynamicChildren && n1.dynamicChildren) {
// patchFlag > 0说明存在编译时优化标记
// patchFlag & 64 说明子节点顺序稳定,标记为STABLE_FRAGMENT
// 新旧节点均有动态子节点数组
// 动态子节点比对
patchBlockChildren(n1.dynamicChildren, dynamicChildren, container, parentComponent, parentSuspense, namespace, slotScopeIds)
// 当Fragment是根节点或带key时处理静态子节点,更新静态节点的引用
if (n2.key != null || parentComponent && n2 === parentComponent.subTree) {
traverseStaticChildren(n1, n2, true)
}
} else {
// 全量子节点比对
patchChildren(n1, n2, container, fragmentEndAnchor, parentComponent, parentSuspense, namespace, slotScopedIds, optimized)
}
}
}
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
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
patchBlockChildren
是仅比对动态子节点,跳过静态节点比对,可以大幅提升性能。
# processElement
处理元素
processElement
用于处理普通Element
元素。
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => {
// 处理命名空间
if (n2.type === "svg") {
namespace = "svg";
} else if (n2.type === "math") {
namespace = "mathml";
}
// 若旧节点n1为null,则调用mountElement方法挂载n2新节点
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
} else {
// 否则调用patchElement更新节点
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
}
};
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
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
# processComponent
处理组件
processComponent
用于更新或挂载组件。
processComponent
的源码实现如下:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => {
n2.slotScopeIds = slotScopeIds;
if (n1 == null) {
// 若旧节点n1为null,则挂载n2新节点,或者激活新节点n2
if (n2.shapeFlag & 512) {
// 判断shapeFlag,若是keepAlive包裹的组件,则激活该组件
parentComponent.ctx.activate(
n2,
container,
anchor,
namespace,
optimized
);
} else {
// 调用mountComponent挂载新节点n2
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
optimized
);
}
} else {
// 调用updateComponent更新组件
updateComponent(n1, n2, optimized);
}
};
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
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
编辑 (opens new window)
上次更新: 2025/09/23, 10:36:42