源码浅析Vue3中的组件挂载
前言
前面我们一起探讨了 Vue3 的 响应式原理 和 编译过程,render函数我们已经拿到了,那具体到底要怎么用呢?这一节,我们就开启一个新篇章——组件的挂载。
组件挂载/更新函数——setupRenderEffect
组件挂载和更新的核心函数都是setupRenderEffect,这里我们就从setupRenderEffect函数作为切入点:
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函数是如何处理挂载的:
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
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:
<div> <span> {{x}} </span> <div>123</div> </div>
经过编译后,生成的render函数是这个样子的:
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函数又做了什么操作:
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
: 挂载的参考元素
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函数
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。
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,改回本名瞬间就直观了很多~
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中,完成最终呈现。
到此这篇关于源码浅析Vue3中的组件挂载的文章就介绍到这了,更多相关Vue3组件挂载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
vue按需加载组件webpack require.ensure的方法
本篇文章主要介绍了vue按需加载组件webpack require.ensure的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2017-12-12
最新评论