前言
前面我们一起探讨了 Vue3 的 响应式原理 和 编译过程 ,render函数我们已经拿到了,那具体到底要怎么用呢?这一节,我们就开启一个新篇章------组件的挂载。
组件挂载/更新函数------setupRenderEffect
组件挂载和更新的核心函数都是setupRenderEffect ,这里我们就从setupRenderEffect函数作为切入点:
js
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 组件尚未挂载,执行挂载操作
...
} else {
// 组件已经挂载,执行更新操作
...
};
// 初始化响应式副作用函数
const effect = instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope
);
const update = instance.update = () => effect.run();
update.id = instance.uid;
...
// 执行副作用函数
update();
};
我们可以看到在 setupRenderEffect 函数中:
- 首先,定义了一个 组件挂载/更新函数 componentUpdateFn ,该函数会根据组件实例的是否已经挂载来进行不同的操作。
- 然后,将 挂载/更新函数 componentUpdateFn 包装为一个 effect副作用函数。
- 最后,执行副作用函数 ,完成挂载或更新操作。
可以看出,组件更新的核心就在于 componentUpdateFn 函数,接下来我们深入来看一下这个函数内部都执行了哪些操作。
componentUpdateFn
我们首先来看实例尚未挂载 的情况下,componentUpdateFn函数是如何处理挂载的:
js
const componentUpdateFn = () => {
// 组件尚未挂载,执行挂载操作
if (!instance.isMounted) {
let vnodeHook;
const { el, props } = initialVNode;
const { bm, m, parent } = instance;
...
// 将实例上的allowRecurse属性(允许递归)设置为false
toggleRecurse(instance, false);
// 如果存在onBeforeMount生命周期函数
if (bm) {
// 执行onBeforeMount中的函数
invokeArrayFns(bm);
}
...
// 将实例上的allowRecurse属性(允许递归)设置为true
toggleRecurse(instance, true);
if (el && hydrateNode) {
...
} else {
// 生成子树的vnode
const subTree = (instance.subTree = renderComponentRoot(instance));
// 挂载子树vnode到容器中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
initialVNode.el = subTree.el;
}
...
// 将实例上的isMounted属性设置为true
instance.isMounted = true;
initialVNode = container = anchor = null;
} else {
// 组件已经挂载过,执行更新操作
...
};
componentUpdateFn在处理组件挂载时主要做的事情就是:
- 首先,判断组件是否存在beforeMount生命周期函数,如果存在,则执行内部定义的函数。
- 然后,根据实例
instance
生成子树vnode。 - 之后,通过patch函数 ,将子树vnode挂载到容器 。(因为目前是挂载阶段,所以patch函数 第一个参数默认设定为了null)
- 最后,将对应的属性值isMounted进行相关配置,将变量指针置空。
接下来,我们进入renderComponentRoot函数 ,看一看生成子树vnode的整个过程是怎样的。
生成vnode的函数------renderComponentRoot
ts
function renderComponentRoot( instance: ComponentInternalInstance ): VNode {
...
let result
...
const proxyToUse = withProxy || proxy
// 取出render函数,并执行
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
...
return result
}
上面我们抽离出该函数的核心 ,可以看到,renderComponentRoot函数 的关键逻辑就是执行了render函数。
前面章节中,我们已经介绍了render函数生成的过程,我们还用之前的例子:
模板template:
html
<div>
<span> {{x}} </span>
<div>123</div>
</div>
经过编译后,生成的render函数是这个样子的:
js
function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, _toDisplayString(x), 1 /* TEXT */),
_hoisted_1
]))
}
}
最终,我们通过执行上面的render函数 ,得到节点的虚拟DOM 也就是vnode
:
然后,我们将这样的一个数据结构(vnode )传入normalizeVNode函数 中做进一步的处理,我们看一下normalizeVNode函数又做了什么操作:
js
function normalizeVNode(child) {
// 如果节点vnode为空,则创建为注释节点
if (child == null || typeof child === "boolean") {
return createVNode(Comment);
} else if (isArray(child)) {
// 如果节点为数组,则在外层包裹一层根节点fragment
return createVNode(
Fragment,
null,
child.slice()
);
} else if (typeof child === "object") {
// 如果是对象形式
return cloneIfMounted(child);
} else {
// 其他情况,比如创建文本类型的节点
return createVNode(Text, null, String(child));
}
}
function cloneIfMounted(child) {
// 如果节点已经挂载,则直接返回对应的vnode,否则克隆一份返回
return child.el === null && child.patchFlag !== -1 /* HOISTED */ || child.memo ? child : cloneVNode(child);
}
在这个函数中会对传入的参数进行分情况讨论:
- 如果参数
vnode
为空 ,则创建为注释节点。 - 如果参数
vnode
为数组 ,则在外层包裹一层根节点Fragment,再执行创建vnode的函数。 - 如果参数
vnode
为对象 形式,则直接返回或克隆该节点vnode。 - 其他情况,主要是像文本类型节点的处理。
我们传入的 child参数 是一个对象 形式,所以会最终执行的是cloneIfMounted函数 ,而这个函数中,会去判断 vnode节点 是否已经被挂载过,如果已经执行过挂载操作,那么其 vnode
的el属性 上就会被赋值,该函数就直接将原vnode 节点返回,否则,执行拷贝操作再返回。
这里,因为的组件在该阶段还未挂载,所以normalizeVNode函数 最终的返回结果也是直接将上面render函数生成的vnode 直接返回,而我们最终renderComponentRoot函数 的返回值同样也是执行render函数得到的vnode。
至此,我们大概理清楚了生成子树vnode
的函数renderComponentRoot 的逻辑,它的主要工作就是通过执行模板编译后生成的render函数,再进行相应的处理,得到最终的vnode。
挂载/更新函数patch
接下来我们开启下一个环节,也是vue中极其重要的一个函数------patch函数。
patch直译过来就是"补丁 "的意思,可以理解为在vue中,组件的挂载和更新都是通过打"补丁 "的方式来进行的。当然,打"补丁"前要先比对一下,看看两个节点到底是哪些信息不一样了,然后再进行定点的更新。
在进入patch函数之前先说明一下patch函数的几个关键参数:
n1
: 旧vnode节点n2
: 新vnode节点container
: 挂载的容器anchor
: 挂载的参考元素
js
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => {
// 如果新旧vnode节点相同,则无须patch
if (n1 === n2) {
return;
}
// 如果新旧vnode节点,type类型不同,则直接卸载旧节点
// 这里isSameVNodeType会判断规则为n1.type === n2.type && n1.key === n2.key
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1);
unmount(n1, parentComponent, parentSuspense, true);
n1 = null;
}
...
const { type, ref: ref2, shapeFlag } = n2;
// 根据新节点的类型,采用不同的函数进行处理
switch (type) {
// 处理文本
case Text:
processText(n1, n2, container, anchor);
break;
// 处理注释
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
// 处理静态节点
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
} else if (true) {
patchStaticNode(n1, n2, container, isSVG);
}
break;
// 处理Fragment
case Fragment:
// Fragment
...
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
// element类型
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
} else if (shapeFlag & 6 /* COMPONENT */) {
// 组件
...
} else if (shapeFlag & 64 /* TELEPORT */) {
// teleport
...
} else if (shapeFlag & 128 /* SUSPENSE */) {
// suspense
...
} else if (true) {
warn2("Invalid VNode type:", type, `(${typeof type})`);
}
}
...
};
patch函数整体的处理逻辑就是:
- 比对新旧节点,如果新旧节点相同,则无须处理。
- 如果新旧节点的类型不同 ,则直接将旧节点卸载 (我们这一节主要研究挂载阶段,所以旧节点为null,可以先不关注这一点)。
- 根据新节点的类型,再分情况进行处理。
这里,我们就用处理element类型来举例,看一下processElement函数,其他情况同理。
processElement函数
js
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
isSVG = isSVG || n2.type === "svg";
if (n1 == null) {
// 如果存在旧节点
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
} else {
// 旧节点不存在
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
);
}
};
processElement函数 ,会根据旧节点是否存在进行分情况讨论,这里我们主要看挂载阶段的函数------mountElement。
js
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let el;
let vnodeHook;
const { type, props, shapeFlag, transition, dirs } = vnode;
// 创建真实DOM结构,并将其保存在在vnode的el属性上
el = vnode.el = hostCreateElement(
vnode.type,
isSVG,
props && props.is,
props
);
// 处理文本节点
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
hostSetElementText(el, vnode.children);
} else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 如果节点类型是数组,则递归的对子节点进行处理
mountChildren(
vnode.children,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== "foreignObject",
slotScopeIds,
optimized
);
}
// 处理vnode上的指令相关内容,并执行指令的生命周期钩子函数
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, "created");
}
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
// 处理props相关内容
if (props) {
for (const key in props) {
if (key !== "value" && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children,
parentComponent,
parentSuspense,
unmountChildren
);
}
}
if ("value" in props) {
hostPatchProp(el, "value", null, props.value);
}
if (vnodeHook = props.onVnodeBeforeMount) {
invokeVNodeHook(vnodeHook, parentComponent, vnode);
}
}
...
// 处理vnode上的指令相关内容,并执行指令的生命周期钩子函数
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, "beforeMount");
}
...
// 将dom挂载到container
hostInsert(el, container, anchor);
...
};
在mountElement函数 中,我们终于创建出了期待已久的真实DOM结构。该函数的逻辑为:
- 根据vnode创建出真实DOM 结构,并保存在el属性上。
- 根据子节点类型来进行不同的操作:
- 文本类型,直接生成文本节点
- 数组类型,则递归的处理子节点
- 对vnode的指令 以及props内容进行处理。
- 最后将生成的DOM挂载到容器container上,也就最终呈现在页面上了。
这里可能有的朋友就是想看一看document.createElement
这种API到底在哪里,那就提一下hostCreateElement函数:
hostCreateElement函数 在源码中是通过解构赋值并重命名得来的,它原来的名字叫createElement,改回本名瞬间就直观了很多~
js
createElement: (tag, isSVG, is, props) => {
const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : void 0);
if (tag === "select" && props && props.multiple != null) {
;
el.setAttribute("multiple", props.multiple);
}
return el;
}
// hostInsert指向的函数就是insert
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
两个工具函数的逻辑也很好理解,就不再多说了吧。总之,终于是看到了document.createElement
就是舒服了^_^
最后
这一节,我们深入研究了Vue3中,组件的挂载逻辑,整个的流程虽然过程繁琐,但要做的事比较清晰,总结下来就是:
- 判断当前vnode是否已经进行过挂载操作,来决定是进行挂载流程还是更新流程。
- 进入挂载流程。
- 执行模板编译阶段生成的render函数 ,得到虚拟dom(vnode)。
- 通过patch函数 ,对vnode 进行分类处理,同时在这个阶段创建出真实的DOM结构。
- 将创建的DOM挂载到容器container中,完成最终呈现。