patch中DOM元素的挂载解析
# 概览
在patch
方法中,若新虚拟节点n2
的shapeFlag
判断是DOM元素,则会调用processElement
方法处理元素的更新或挂载,若旧虚拟节点n1
为null
,则会调用mountElement
挂载DOM元素。
mountElement
方法是vue3中挂载元素节点的核心函数,它负责将虚拟节点VNode
转换为真实的DOM元素并插入到容器中。
# 源码解析
- 参数介绍
mountElement
方法的参数说明如下:
vnode
:要挂载的虚拟节点container
:挂载的容器元素anchor
:插入的参考位置锚点,新元素会插入到这个锚点之前parentComponent
:父组件实例parentSuspense
:父级Suspense
实例namespace
:命名空间,如SVG
slotScopeIds
:插槽作用域ID ,可以用于优化optimized
:是否处于优化模式,用于静态节点优化源码分析
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized) => {
let el;
let vnodeHook;
const { props, shapeFlag, transition, dirs } = vnode;
// 创建真实DOM元素
el = vnode.el = hostCreateElement(
vnode.type,
namespace,
props && props.is,
props
);
// 处理子节点
if (shapeFlag & 8) {
// 处理文本节点
hostSetElementText(el, vnode.children);
} else if (shapeFlag & 16) {
// 若节点有子节点,递归挂载子节点
mountChildren(
vnode.children,
el,
null,
parentComponent,
parentSuspense,
resolveChildrenNamespace(vnode, namespace),
slotScopeIds,
optimized
);
}
// 若节点有指令
if (dirs) {
// 则调用指令的created钩子
invokeDirectiveHook(vnode, null, parentComponent, "created");
}
// 设置作用域ID,用于Scoped CSS
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
// 处理属性
if (props) {
// 遍历props
for (const key in props) {
// 将属性设置到元素上,跳过value和保留属性
if (key !== "value" && !shared.isReservedProp(key)) {
// 调用hostPatchProp设置属性
hostPatchProp(el, key, null, props[key], namespace, parentComponent);
}
}
// 单独处理value属性
if ("value" in props) {
hostPatchProp(el, "value", null, props.value, namespace);
}
// 如果有onVnodeBeforeMount钩子,则调用它
if (vnodeHook = props.onVnodeBeforeMount) {
invokeVNodeHook(vnodeHook, parentComponent, vnode);
}
}
// 调用指令的beforeMount钩子
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, "beforeMount");
}
// 处理过渡效果
const needCallTransitionHooks = needTransition(parentSuspense, transition);
if (needCallTransitionHooks) {
transition.beforeEnter(el);
}
// 插入元素
hostInsert(el, container, anchor);
// 执行挂载后的钩子,推入到队列中
if ((vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
needCallTransitionHooks && transition.enter(el);
dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
}, parentSuspense);
}
}
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
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
# 辅助方法
# mountChildren
mountElement
方法挂载DOM元素时,若虚拟节点有子节点,则会调用mountChildren
挂载子节点。mountChildren
就是遍历子节点,然后调用patch
方法,在patch
方法中根据子节点的type
或shapeFlag
进行挂载。
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, start = 0) => {
for (let i = start; i < children.length; i++) {
const child = children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]);
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在mountChildren
中调用patch
方法传入的child
就是新节点,若optimized
为true
,则说明是优化模式,会调用cloneIfMounted
生成节点;否则调用normalizeVNode
,该方法会生成虚拟节点。
# cloneIfMounted
cloneIfMounted
方法就是判断虚拟节点是否挂载了,若挂载了,就调用cloneVNode
方法进行克隆VNode
;否则直接返回虚拟节点。
function cloneIfMounted(child) {
// 虚拟节点未挂载时,el元素为null,patchFlag为-1表示没有缓存
return child.el === null && child.patchFlag !== -1 || child.memo ? child : cloneVNode(child);
}
1
2
3
4
2
3
4
# cloneVNode
cloneVNode
方法用于拷贝复制一份虚拟节点。
function cloneVNode(vnode, extraProps, mergeRef = false, cloneTransition = false) {
const { props, ref, patchFlag, children, transition } = vnode;
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props;
const cloned = {
__v_isVNode: true, // 虚拟节点标志位
__v_skip: true, // 跳过某些优化
type: vnode.type, // 节点类型
props: mergedProps, // 合并后的属性
key: mergedProps && normalizeKey(mergedProps), // 规范化后的可以
ref: extraProps && extraProps.ref ? (
mergeRef && ref ? shared.isArray(ref) ? ref.concat(normalizeRef(extraProps)) : [ref, normalizeRef(extraProps)] : normalizeRef(extraProps)
) : ref,
scopeId: vnode.scopeId,
slotScopeIds: vnode.slotScopeIds,
children: children,// 直接复用子节点
target: vnode.target,
targetStart: vnode.targetStart,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,
patchFlag: extraProps && vnode.type !== Fragment ? patchFlag === -1 ? 16 : patchFlag | 16 : patchFlag,
dynamicProps: vnode.dynamicProps,
dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext,
dirs: vnode.dirs,
transition,
component: vnode.component,
suspense: vnode.suspense,
ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
placeholder: vnode.placeholder,
el: vnode.el,
anchor: vnode.anchor,
ctx: vnode.ctx,
ce: vnode.ce
};
if (transition && cloneTransition) {
setTransitionHooks(
cloned,
transition.clone(cloned)
);
}
return cloned;
}
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
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
# normalizeVNode
function normalizeVNode(child) {
if (child == null || typeof child === "boolean") {
return createVNode(Comment);
} else if (shared.isArray(child)) {
return createVNode(
Fragment,
null,
child.slice()
);
} else if (isVNode(child)) {
return cloneIfMounted(child);
} else {
return createVNode(Text, null, String(child));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# createVNode
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
type = Comment;
}
if (isVNode(type)) {
const cloned = cloneVNode(
type,
props,
true
);
if (children) {
normalizeChildren(cloned, children);
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & 6) {
currentBlock[currentBlock.indexOf(type)] = cloned;
} else {
currentBlock.push(cloned);
}
}
cloned.patchFlag = -2;
return cloned;
}
if (isClassComponent(type)) {
type = type.__vccOpts;
}
if (props) {
props = guardReactiveProps(props);
let { class: klass, style } = props;
if (klass && !shared.isString(klass)) {
props.class = shared.normalizeClass(klass);
}
if (shared.isObject(style)) {
if (reactivity.isProxy(style) && !shared.isArray(style)) {
style = shared.extend({}, style);
}
props.style = shared.normalizeStyle(style);
}
}
const shapeFlag = shared.isString(type) ? 1 : isSuspense(type) ? 128 : isTeleport(type) ? 64 : shared.isObject(type) ? 4 : shared.isFunction(type) ? 2 : 0;
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
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
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
# setScopeId
const setScopeId = (el, vnode, scopeId, slotScopeIds, parentComponent) => {
if (scopeId) {
hostSetScopeId(el, scopeId)
}
if (slotScopeIds) {
for (let i = 0; i < slotScopeIds.length; i++) {
hostSetScopeId(el, slotScopeIds[i])
}
}
if (parentComponent) {
let subTree = parentComponent.subTree;
if (vnode === subTree || isSuspense(subTree.type) && (subTree.ssContent === vnode || subTree.ssFallback === vnode)) {
const parentVNode = parentComponent.vnode;
setScopeId(el, parentVNode, parentVNode.scopeId, parentVNode.slotScopeIds, parentComponent.parent)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
编辑 (opens new window)
上次更新: 2025/10/03, 03:52:19