从我最开始讲createApp
的时候,里面就有组件挂载的逻辑,但是我并没有讲,今天我们就来讲一下组件挂载的逻辑。
在createApp
中,我们使用mount
方法来将组件挂载到DOM
上,这里我们先回顾一下mount
方法的实现:
function mount(rootContainer, isHydrate, isSVG) { // 判断是否已经挂载 if (!isMounted) { // 这里的 #5571 是一个 issue 的 id,可以在 github 上搜索,这是一个在相同容器上重复挂载的问题,这里只做提示,不做处理 // #5571 if ((process.env.NODE_ENV !== 'production') && rootContainer.__vue_app__) { warn(`There is already an app instance mounted on the host container.\n` + ` If you want to mount another app on the same host container,` + ` you need to unmount the previous app by calling \`app.unmount()\` first.`); } // 通过在 createApp 中传递的参数来创建虚拟节点 const vnode = createVNode(rootComponent, rootProps); // store app context on the root VNode. // this will be set on the root instance on initial mount. // 上面有注释,在根节点上挂载 app 上下文,这个上下文会在挂载时设置到根实例上 vnode.appContext = context; // HMR root reload // 热更新 if ((process.env.NODE_ENV !== 'production')) { context.reload = () => { render(cloneVNode(vnode), rootContainer, isSVG); }; } // 通过其他的方式挂载,这里不一定指代的是服务端渲染,也可能是其他的方式 // 这一块可以通过创建渲染器的源码可以看出,我们日常在客户端渲染,不会使用到这一块,这里只是做提示,不做具体的分析 if (isHydrate && hydrate) { hydrate(vnode, rootContainer); } // 其他情况下,直接通过 render 函数挂载 // render 函数在 createRenderer 中定义,传递到 createAppAPI 中,通过闭包缓存下来的 else { render(vnode, rootContainer, isSVG); } // 挂载完成后,设置 isMounted 为 true isMounted = true; // 设置 app 实例的 _container 属性,指向挂载的容器 app._container = rootContainer; // 挂载的容器上挂载 app 实例,也就是说我们可以通过容器找到 app 实例 rootContainer.__vue_app__ = app; // 非生产环境默认开启 devtools,也可以通过全局配置来开启或关闭 // __VUE_PROD_DEVTOOLS__ 可以通过自己使用的构建工具来配置,这里只做提示 if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) { app._instance = vnode.component; devtoolsInitApp(app, version); } // 返回 app 实例,这里不做具体的分析 return getExposeProxy(vnode.component) || vnode.component.proxy; } // 如果已经挂载过则输出提示消息,在非生产环境下 else if ((process.env.NODE_ENV !== 'production')) { warn(`App has already been mounted.\n` + `If you want to remount the same app, move your app creation logic ` + `into a factory function and create fresh app instances for each ` + `mount - e.g. \`const createMyApp = () => createApp(App)\``); } }
在我讲createApp
的时候其实已经知道了,组件的挂载其实就是通过render
函数来挂载的,上面的mount
方法中其实也可以看出来是使用render
函数来挂载的,简化之后的mount
方法如下:
function mount(rootContainer, isHydrate) { // createApp 中传递的参数在我们这里肯定是一个对象,所以这里不做创建虚拟节点的操作,而是模拟一个虚拟节点 const vnode = { type: rootComponent, children: [], component: null, } // 通过 render 函数渲染虚拟节点 render(vnode, rootContainer); // 返回 app 实例 return vnode.component }
上面的代码以及解释来自:【源码&库】在调用 createApp 时,Vue 为我们做了那些工作?
这次我们直接进入正题,看一下render
函数的实现,如果没有看过我上面写的createApp
的话,这里建议可以去看看;
因为render
函数是在baseCreateRenderer
中定义的,这一篇不会讲怎么找baseCreateRenderer
已经它的实现,如果初看可能找不到render
函数。
render
render
函数的实现如下:
/** * * @param vnode 虚拟节点 * @param container 容器 * @param isSVG 是否是 svg */ const render = (vnode, container, isSVG) => { // 如果 vnode 不存在,说明是卸载 if (vnode == null) { // 如果容器上有 vnode 才执行卸载操作,和最后一行对应 if (container._vnode) { // 卸载 unmount(container._vnode, null, null, true); } } else { // patch 核心,等会主讲 patch(container._vnode || null, vnode, container, null, null, null, isSVG); } // 触发一些需要在数据更新之前执行的回调函数 flushPreFlushCbs(); // 触发一些需要在数据更新之后执行的回调函数 flushPostFlushCbs(); // 将 vnode 赋值给容器的 _vnode 属性 container._vnode = vnode; };
render
函数的实现很简单,就是判断vnode
是否存在,如果存在则执行patch
函数,如果不存在则执行unmount
函数。
unmount
函数不是我们这章主讲的,我们直接来看patch
函数。
patch
patch
函数的实现如下:
/** * * @param n1 上一个虚拟节点 * @param n2 当前虚拟节点 * @param container 容器 * @param anchor 锚点 * @param parentComponent * @param parentSuspense * @param isSVG * @param optimized */ const patch = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = (process.env.NODE_ENV !== 'production') && isHmrUpdating ? false : !!n2.dynamicChildren ) => { // 相同的虚拟节点,直接返回 if (n1 === n2) { return; } // patching & not same type, unmount old tree // 如果 n1 存在,且 n1 和 n2 不是同一类型的虚拟节点,则卸载 n1 if (n1 && !isSameVNodeType(n1, n2)) { // 用 n1 的下一个兄弟节点作为锚点 anchor = getNextHostNode(n1); // 卸载 n1 unmount(n1, parentComponent, parentSuspense, true); n1 = null; } // 如果确定 n2 这个节点没有动态内容 if (n2.patchFlag === -2 /* PatchFlags.BAIL */) { // 没有动态内容,没必要再进行优化了 optimized = false; // 将动态子节点置空,避免不必要的操作 n2.dynamicChildren = null; } // 获取 n2 的类型 // type 表示当前节点的类型,可以是字符串、标签名、组件对象、函数组件等 // ref 表示当前节点的 ref 属性,如果定义了 ref 属性,则 ref 指向这个节点的实例 // shapeFlag 表示当前节点的标识,是一个二进制数 const {type, ref, 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 ((process.env.NODE_ENV !== 'production')) { patchStaticNode(n1, n2, container, isSVG); } break; // Fragment 节点 case Fragment: processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); break; // 其他节点 default: // 如果是元素节点 if (shapeFlag & 1 /* ShapeFlags.ELEMENT */) { processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } // 如果是组件节点 else if (shapeFlag & 6 /* ShapeFlags.COMPONENT */) { processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } // 如果是 Teleport 组件包裹的节点 else if (shapeFlag & 64 /* ShapeFlags.TELEPORT */) { type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals); } // 如果是 Suspense 组件包裹的节点 else if (shapeFlag & 128 /* ShapeFlags.SUSPENSE */) { type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals); } // 不是以上类型的节点,报错 else if ((process.env.NODE_ENV !== 'production')) { warn('Invalid VNode type:', type, `(${ typeof type })`); } } // set ref // 如果 n2 定义了 ref 属性,则调用 setRef 函数 if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2); } };
patch
函数的参数比较多,代码也很多,不要着急,我们一步一步来看。
首先我们目的学习组件是如何挂载的,那么我们只需要关注switch
语句中的代码即可。
switch
语句中一共有八种情况:
- 文本节点
- 注释节点
- 静态节点
- Fragment 节点
- 元素节点
- 组件节点
- Teleport 组件包裹的节点
- Suspense 组件包裹的节点
这其中Teleport
和Suspense
组件包裹的节点,我们暂时不用关注,因为这两个组件还没有讲到,我们先来看看前面的几种情况。
文本节点的挂载
文本节点挂载是通过processText
函数来实现的,processText
函数的代码如下:
/** * 处理文本节点 * @param n1 旧的虚拟节点 * @param n2 新的虚拟节点 * @param container 容器 * @param anchor 锚点 */ const processText = (n1, n2, container, anchor) => { // n1 不存在,说明是新节点,直接创建文本节点 if (n1 == null) { hostInsert((n2.el = hostCreateText(n2.children)), container, anchor); } else { // n1 存在,说明是更新节点,复用旧节点 const el = (n2.el = n1.el); // 如果新旧节点的文本内容不一致,则更新文本内容 if (n2.children !== n1.children) { hostSetText(el, n2.children); } } };
processText
的实现还是比较简单的,这里主要关心的是hostInsert
、hostCreateText
和hostSetText
这三个函数。
但是这三个函数是在createApp
函数实现的时候定义的,又说到了createApp
函数,还是建议没看过我之前的文章的同学先去看看。
注释节点的挂载
注释节点挂载是通过processCommentNode
函数来实现的,processCommentNode
函数的代码如下:
const processCommentNode = (n1, n2, container, anchor) => { if (n1 == null) { hostInsert((n2.el = hostCreateComment(n2.children || '')), container, anchor); } else { // there's no support for dynamic comments n2.el = n1.el; } };
内部逻辑和processText
函数类似,这里代码量也不多,主要是将hostCreateText
换成了hostCreateComment
,可以自己花个几秒钟对比一下;
静态节点的挂载
静态节点挂载在switch
语句中的代码不同于文本节点和注释节点,因为静态节点代表的就是不会发生变化的节点,所以我们不需要每次都去更新节点,只需要在第一次挂载的时候创建节点即可。
而在开发环境下,我们可能会手动修改静态节点的内容,这时候我们需要重新渲染静态节点,所以在开发环境下,会执行patchStaticNode
函数,而在生产环境下,会执行processText
函数。
case Static: // 直接挂载静态节点 if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG); } // 在开发环境下,如果静态节点发生了变化,则打补丁 else if ((process.env.NODE_ENV !== 'production')) { patchStaticNode(n1, n2, container, isSVG); }
mountStaticNode
和patchStaticNode
函数的代码如下:
const mountStaticNode = (n2, container, anchor, isSVG) => { [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG, n2.el, n2.anchor); }; /** * Dev / HMR only */ const patchStaticNode = (n1, n2, container, isSVG) => { // static nodes are only patched during dev for HMR if (n2.children !== n1.children) { const anchor = hostNextSibling(n1.anchor); // remove existing removeStaticNode(n1); [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG); } else { n2.el = n1.el; n2.anchor = n1.anchor; } };
代码也很简单,这里一个细节就是通过解构赋值的方式将hostInsertStaticContent
函数的返回值赋值给n2.el
和n2.anchor
;
就是不知道为啥patchStaticNode
函数中的else
没有用解构赋值的方式,而是直接赋值的。
Fragment 节点的挂载
Fragment 节点的挂载是通过processFragment
函数来实现的,processFragment
函数的代码如下:
const processFragment = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { // 如果旧的虚拟节点不存在,则创建一个文本节点作为开始和结束的锚点标记 const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText('')); const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText('')); // 获取新节点(n2)中的 patchFlag、dynamicChildren、fragmentSlotScopeIds let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2; // 只有在开发环境下,且是 HMR 更新或者是根节点的 Fragment 才会执行 if ((process.env.NODE_ENV !== 'production') && // #5523 dev root fragment may inherit directives (isHmrUpdating || patchFlag & 2048 /* PatchFlags.DEV_ROOT_FRAGMENT */)) { // HMR updated / Dev root fragment (w/ comments), force full diff patchFlag = 0; optimized = false; dynamicChildren = null; } // check if this is a slot fragment with :slotted scope ids // 检查这是否是一个具有 :slotted 作用域 ID 的插槽片段 if (fragmentSlotScopeIds) { // 如果 slotScopeIds 存在,则将 fragmentSlotScopeIds 和 slotScopeIds 进行合并 slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; } // 如果没有旧节点就直接挂载 if (n1 == null) { hostInsert(fragmentStartAnchor, container, anchor); hostInsert(fragmentEndAnchor, container, anchor); // a fragment can only have array children // since they are either generated by the compiler, or implicitly created // from arrays. // 一个片段只能有数组子节点,因为它们要么由编译器生成,要么隐式地从数组中创建。 mountChildren(n2.children, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } else { if (patchFlag > 0 && patchFlag & 64 /* PatchFlags.STABLE_FRAGMENT */ && dynamicChildren && // #2715 the previous fragment could've been a BAILed one as a result // of renderSlot() with no valid children // #2715 之前的片段可能是一个 BAILed 的片段,因为 renderSlot() 没有有效的子节点 n1.dynamicChildren) { // a stable fragment (template root or <template v-for>) doesn't need to // patch children order, but it may contain dynamicChildren. // 一个稳定的片段(模板根或 <template v-for>)不需要修补子节点顺序,但它可能包含 dynamicChildren。 patchBlockChildren(n1.dynamicChildren, dynamicChildren, container, parentComponent, parentSuspense, isSVG, slotScopeIds); if ((process.env.NODE_ENV !== 'production') && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2); } else if ( // #2080 if the stable fragment has a key, it's a <template v-for> that may // get moved around. Make sure all root level vnodes inherit el. // #2134 or if it's a component root, it may also get moved around // as the component is being moved. // #2080 如果稳定片段有一个 key,那么它是一个 <template v-for> 可能会被移动。确保所有根级 vnode 继承 el。 // #2134 或者如果它是一个组件根,它也可能会被移动,因为组件正在被移动。 n2.key != null || (parentComponent && n2 === parentComponent.subTree)) { traverseStaticChildren(n1, n2, true /* shallow */); } } else { // keyed / unkeyed, or manual fragments. // for keyed & unkeyed, since they are compiler generated from v-for, // each child is guaranteed to be a block so the fragment will never // have dynamicChildren. // 对于有 key 和无 key 的,因为它们是从 v-for 编译器生成的,所以每个子节点都保证是一个块,因此片段永远不会有 dynamicChildren。 patchChildren(n1, n2, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); } } };
processFragment
函数中的代码比较多,而且注释也比较多,所以这里就不一一解释了,大家可以自己去捉摸着看看;
Element 节点的挂载
Element
节点的挂载是通过processElement
函数来实现的,processElement
函数的代码如下:
这里会涉及到
shapeFlag
的使用,shapeFlag
就是在createVNode
函数中,通过判断type
的类型来赋值的,这里只做提示,具体的可以看看createVNode
函数;
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); } };
还是老样子,如果没有旧节点就直接挂载,如果有旧节点就进行更新,这里主要讲的是组件的挂载,所以就暂时先不跟进去详细介绍了;
组件的挂载
组件的挂载是通过processComponent
函数来实现的,processComponent
函数的代码如下:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => { n2.slotScopeIds = slotScopeIds; // 如果没有旧节点就直接挂载 if (n1 == null) { // 如果是 keep-alive 组件,就调用父组件的 activate 方法 if (n2.shapeFlag & 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */) { parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized); } // 否则就直接挂载 else { mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized); } } else { // 更新组件 updateComponent(n1, n2, optimized); } };
看了上面那么多类型的节点挂载,其实发现组件的挂载和上面的节点挂载都差不多,主要还是看mountComponent
函数和updateComponent
函数的实现;
mountComponent
mountComponent
函数的代码如下:
/** * @param initialVNode 组件的 vnode * @param container 容器 * @param anchor 锚点 * @param parentComponent 父组件 * @param parentSuspense 父 suspense * @param isSVG 是否是 svg * @param optimized 是否优化 */ const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => { // 创建组件实例 const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)); // inject renderer internals for keepAlive // 为 keepAlive 注入 renderer if (isKeepAlive(initialVNode)) { instance.ctx.renderer = internals; } // resolve props and slots for setup context // 为 setup 上下文解析 props 和 slots setupComponent(instance); // setup() is async. This component relies on async logic to be resolved // before proceeding // setup() 是异步的。在挂载之前需要等待异步逻辑完成 if (instance.asyncDep) { // 如果有 parentSuspense,就注册依赖 parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect); // Give it a placeholder if this is not hydration // TODO handle self-defined fallback // 如果这不是"hydration"(首次渲染直接输出HTML而非先输出一个空的HTML再用 JS 进行“hydration”),那么给它一个占位符。 // TODO 处理自定义的 fallback // 如果没有 el 属性,也就是说当前组件不存在,那么就创建一个占位符 if (!initialVNode.el) { // 通过注释节点来创建一个占位符 const placeholder = (instance.subTree = createVNode(Comment)); processCommentNode(null, placeholder, container, anchor); } return; } // 执行组件的渲染函数 setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized); };
上面的代码移除了开发环境下的一些代码,后面的代码讲解也会默认去掉开发环境下的代码;
上面的代码其实并不多,抛开边界情况,只执行了三个函数,createComponentInstance
、setupComponent
、setupRenderEffect
;
createComponentInstance
createComponentInstance
函数的代码如下:
// 在 createApp 的文章中讲到过 createAppContext 函数 const emptyAppContext = createAppContext(); // 组件实例的 uid let uid = 0; function createComponentInstance(vnode, parent, suspense) { const type = vnode.type; // inherit parent app context - or - if root, adopt from root vnode // 继承父应用上下文 - 或者 - 如果是根节点,就从根 vnode 中继承 const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext; // 组件实例 const instance = { uid: uid++, vnode, type, parent, appContext, root: null, next: null, subTree: null, effect: null, update: null, scope: new EffectScope(true /* detached */), render: null, proxy: null, exposed: null, exposeProxy: null, withProxy: null, provides: parent ? parent.provides : Object.create(appContext.provides), accessCache: null, renderCache: [], // local resolved assets components: null, directives: null, // resolved props and emits options propsOptions: normalizePropsOptions(type, appContext), emitsOptions: normalizeEmitsOptions(type, appContext), // emit emit: null, emitted: null, // props default value propsDefaults: EMPTY_OBJ, // inheritAttrs inheritAttrs: type.inheritAttrs, // state ctx: EMPTY_OBJ, data: EMPTY_OBJ, props: EMPTY_OBJ, attrs: EMPTY_OBJ, slots: EMPTY_OBJ, refs: EMPTY_OBJ, setupState: EMPTY_OBJ, setupContext: null, // suspense related suspense, suspenseId: suspense ? suspense.pendingId : 0, asyncDep: null, asyncResolved: false, // lifecycle hooks // not using enums here because it results in computed properties isMounted: false, isUnmounted: false, isDeactivated: false, bc: null, c: null, bm: null, m: null, bu: null, u: null, um: null, bum: null, da: null, a: null, rtg: null, rtc: null, ec: null, sp: null }; // 将组件实例挂载到 ctx 上 instance.ctx = { _: instance }; // 根组件的实例 instance.root = parent ? parent.root : instance; // 组件实例的 emit 函数,将 this 指向组件实例 instance.emit = emit.bind(null, instance); // apply custom element special handling // 应用自定义元素特殊处理 if (vnode.ce) { vnode.ce(instance); } // 返回组件实例 return instance; }
这里就做了一些组件的实例初始化的工作,初始化完成之后,将组件实例返回;
setupComponent
setupComponent
函数的代码如下:
// 是否在 SSR 组件的 setup 中 let isInSSRComponentSetup = false; /** * @param instance 组件实例 * @param isSSR 是否是 SSR * @return {*} */ function setupComponent(instance, isSSR = false) { isInSSRComponentSetup = isSSR; // 获取组件的 props、children const { props, children } = instance.vnode; // 是否是有状态的组件 const isStateful = isStatefulComponent(instance); // 初始化 props initProps(instance, props, isStateful, isSSR); // 初始化 slots initSlots(instance, children); // 有状态的组件调用 setupStatefulComponent 函数 const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined; isInSSRComponentSetup = false; // 返回 setup 函数的返回值 return setupResult; }
setupComponent
函数的作用是初始化组件的 props
、slots
,然后调用 setupStatefulComponent
函数,setupStatefulComponent
函数的作用是调用组件的 setup
函数;
isStatefulComponent
就是通过组件的shapeFlag
来判断组件是否是有状态的组件;
initProps
initProps
函数的代码如下:
function initProps(instance, rawProps, isStateful, // result of bitwise flag comparison isSSR = false) { const props = {}; const attrs = {}; // 通过 Object.defineProperty 将 InternalObjectKey (__vInternal)设置为 1,并且不可枚举 def(attrs, InternalObjectKey, 1); // 通过 Object.create(null) 创建一个空对象,这个对象没有原型链 instance.propsDefaults = Object.create(null); // 设置组件的 props、attrs setFullProps(instance, rawProps, props, attrs); // ensure all declared prop keys are present // 确保所有声明的 prop key 都存在 for (const key in instance.propsOptions[0]) { if (!(key in props)) { props[key] = undefined; } } // 有状态的组件 if (isStateful) { // stateful // 将 props 设置为响应式的 instance.props = isSSR ? props : shallowReactive(props); } else { // 当函数式组件没有声明 props 时,将 attrs 设置为组件的 props if (!instance.type.props) { // functional w/ optional props, props === attrs // 当组件没有声明 props 时,传递给组件的 porps 会自动设置为 attrs,这个时候 props 和 attrs 是等价的 instance.props = attrs; } else { // functional w/ declared props // 函数式组件声明了 props,保存 props instance.props = props; } } // 设置组件的 attrs instance.attrs = attrs; }
这里的关键点是通过 setFullProps
函数将 rawProps
转换为 props
和attrs
,然后通过 shallowReactive
函数将 props
设置为响应式的;
initSlots
initSlots
函数的代码如下:
const initSlots = (instance, children) => { // 如果 vnode 的 shapeFlag 为 32,对应是 SLOTS_CHILDREN if (instance.vnode.shapeFlag & 32 /* ShapeFlags.SLOTS_CHILDREN */) { // “_” 代表的是组件的实例,在 createComponentInstance 中有出现 const type = children._; if (type) { // users can get the shallow readonly version of the slots object through `this.$slots`, // we should avoid the proxy object polluting the slots of the internal instance // 用户可以通过 this.$slots 获取 slots 对象的浅只读版本, // 我们应该避免代理对象污染内部实例的 slots instance.slots = toRaw(children); // make compiler marker non-enumerable // 将编译器标记设置为不可枚举 def(children, '_', type); } else { // 如果 type 不存在,说明没有定义 slots,直接将 children 赋值给 instance.slots normalizeObjectSlots(children, (instance.slots = {})); } } else { // 如果 vnode 的 shapeFlag 不为 32,说明没有定义 slots,直接将 instance.slots 赋值为空对象 instance.slots = {}; // 如果 children 存在,说明有默认插槽,需要进行插槽的规范化 if (children) { normalizeVNodeSlots(instance, children); } } // 将 InternalObjectKey 设置为 1,并且设置为不可枚举,这个在上面的 initProps 函数中有出现 def(instance.slots, InternalObjectKey, 1); };
这里主要是对 slots
进行初始化,如果 vnode
的 shapeFlag
为 32
,则说明定义了 slots
,否则说明没有定义 slots
;
如果定义了 slots
,则会将 children
赋值给 instance.slots
,否则将 instance.slots
赋值为空对象;
如果 children
存在,说明有默认插槽,需要进行插槽的规范化;
这里不过多的深入 props 和 slots 的实现,这里有个大概的意思就可以了,后面会有专门的章节来讲解 props 和 slots 的实现;
setupStatefulComponent
setupStatefulComponent
函数的代码如下:
function setupStatefulComponent(instance, isSSR) { var _a; // instance.type 就是组件的定义,比如 App.vue const Component = instance.type; // 0. create render proxy property access cache // 0. 创建 render 代理属性访问缓存 instance.accessCache = Object.create(null); // 1. create public instance / render proxy // also mark it raw so it's never observed // 1. 创建公共实例 / render 代理 // 同时将其标记为原始的,以便永远不会被观察到 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)); // 2. call setup() // 2. 调用 setup() const { setup } = Component; if (setup) { // 获取 setupContext 对象 const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null); // 设置当前实例 setCurrentInstance(instance); // 暂停追踪依赖 pauseTracking(); // 调用 setup 函数并获取返回值 const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [instance.props, setupContext]); // 恢复追踪依赖 resetTracking(); // 解绑当前实例 unsetCurrentInstance(); // 如果返回值是 Promise if (isPromise(setupResult)) { // 等待 Promise 执行完毕就解绑当前实例 setupResult.then(unsetCurrentInstance, unsetCurrentInstance); // 如果是服务端渲染 if (isSSR) { // return the promise so server-renderer can wait on it // 返回 Promise,以便服务器渲染器可以等待它 return setupResult .then((resolvedResult) => { handleSetupResult(instance, resolvedResult, isSSR); }) .catch(e => { handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */); }); } else { // async setup returned Promise. // bail here and wait for re-entry. // 异步 setup 返回 Promise。 // 在这里中止并等待重新进入。 instance.asyncDep = setupResult; } } else { // 同步 setup 返回值 handleSetupResult(instance, setupResult, isSSR); } } else { // 没有定义 setup 函数,直接调用 finishComponentSetup finishComponentSetup(instance, isSSR); } }
这里主要是对 setup
函数进行调用,如果 setup
函数返回的是 Promise
,则会等待 Promise
执行完毕,否则直接调用 finishComponentSetup
函数;
finishComponentSetup
而finishComponentSetup
函数执行的主要是对template
或者 render
函数的编译,代码如下:
function finishComponentSetup(instance, isSSR, skipOptions) { // 实例上的 type 就是组件的定义,比如 App.vue const Component = instance.type; // template / render function normalization // could be already set when returned from setup() // template / render 函数规范化 // 可能已经在从 setup() 返回时设置了 if (!instance.render) { // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation // is done by server-renderer // 只有在 SSR 时才进行即时编译 - 即时编译 SSR 是由服务器渲染器完成的 if (!isSSR && compile && !Component.render) { // 获取 template,如果当前组件没有定义 template,则会通过合并实例中的属性获取 template // 这里的 template 则可能是 mixins 中的 template const template = Component.template || resolveMergedOptions(instance).template; // 如果 template 存在,则进行编译 if (template) { // 获取全局配置 const { isCustomElement, compilerOptions } = instance.appContext.config; // 获取组件的 delimiters 和 compilerOptions const { delimiters, compilerOptions: componentCompilerOptions } = Component; const finalCompilerOptions = extend(extend({ isCustomElement, delimiters }, compilerOptions), componentCompilerOptions); // 编译 template,得到 render 函数 Component.render = compile(template, finalCompilerOptions); } } // 获取 render 函数 instance.render = (Component.render || NOOP); // for runtime-compiled render functions using `with` blocks, the render // proxy used needs a different `has` handler which is more performant and // also only allows a whitelist of globals to fallthrough. // 对于使用 `with` 块的运行时编译的 render 函数,使用的 render 代理需要一个不同的 `has` 处理程序,该处理程序更高效,并且只允许白名单的全局变量通过。 if (installWithProxy) { installWithProxy(instance); } } // support for 2.x options // 2.x 选项的支持 if (__VUE_OPTIONS_API__ && !(false )) { setCurrentInstance(instance); pauseTracking(); applyOptions(instance); resetTracking(); unsetCurrentInstance(); } }
这里主要是对 template
或者 render
函数的编译,如果 template
存在,则会进行编译,得到 render
函数;
代码中的配置项,例如:
isCustomElement
、compilerOptions
、delimiters
、compilerOptions
等,可以在官网中查到,这里不提供链接了,大家可以自行查阅。
到这里,setup
函数的调用就结束了,接下来就是对 render
函数的调用了;
setupRenderEffect
render
函数的调用,主要是在 mountComponent
函数中,在mountComponent
函数中,最后一行代码如下:
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => { // ... // 执行组件的渲染函数 setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized); };
setupRenderEffect
函数的代码如下:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { // 组件的更新函数 const componentUpdateFn = () => { // ... }; // create reactive effect for rendering // 创建用于渲染的响应式 effect const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope )); // 组件的更新钩子函数 const update = (instance.update = () => effect.run()); // 组件uid,在上面出现过 update.id = instance.uid; // allowRecurse // #1801, #2043 component render effects should allow recursive updates // 允许递归 // #1801,#2043 组件渲染效果应该允许递归更新 toggleRecurse(instance, true); // 执行组件的更新函数 update(); };
这里主要是创建了一个 effect
函数,而这个ReactiveEffect
就是在我之前讲的响应式核心中提到的 ReactiveEffect
,巧了不是?这一下直接把之前的文章都串起来了;
这里首先通过 new ReactiveEffect
创建了一个 effect
函数,里面执行的副作用函数就是 componentUpdateFn
;
后面传了一个调度器,这个调度器就是queueJob(update)
,这个queueJob
函数在讲nextTick
时出现过,这一下全都串起来了;
最后执行了 update
函数,然后就会触发依赖收集,当我们修改了响应式数据时,就会触发依赖更新,从而触发 effect
函数,从而执行 componentUpdateFn
函数;
componentUpdateFn
componentUpdateFn
函数的内部实现主要有两个部分,一个是组件挂载的逻辑,一个是组件更新的逻辑;
我们先来看组件挂载的逻辑:
const componentUpdateFn = () => { // isMounted 表示组件是否已经挂载 if (!instance.isMounted) { let vnodeHook; // initialVNode 是组件的虚拟节点 const { el, props } = initialVNode; // 获取组件的 beforeMount、mounted 钩子函数 和 父组件 const { bm, m, parent } = instance; // 是否是异步组件 const isAsyncWrapperVNode = isAsyncWrapper(initialVNode); // 不允许递归调用 toggleRecurse(instance, false); // beforeMount hook // 执行组件的 beforeMount 钩子函数 if (bm) { invokeArrayFns(bm); } // onVnodeBeforeMount // 执行组件的 onVnodeBeforeMount 钩子函数 if (!isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parent, initialVNode); } // 允许递归调用 toggleRecurse(instance, true); // 执行 hydrateNode if (el && hydrateNode) { // vnode has adopted host node - perform hydration instead of mount. // vnode 已经接管了宿主节点 - 执行 hydration 而不是 mount const hydrateSubTree = () => { instance.subTree = renderComponentRoot(instance); hydrateNode(el, instance.subTree, instance, parentSuspense, null); }; // 如果是异步组件,则执行异步加载 if (isAsyncWrapperVNode) { initialVNode.type.__asyncLoader().then( // note: we are moving the render call into an async callback, // which means it won't track dependencies - but it's ok because // a server-rendered async wrapper is already in resolved state // and it will never need to change. // 注意:我们将渲染调用移动到异步回调中, // 这意味着它不会跟踪依赖关系 - 但这是可以的,因为 // 服务器渲染的异步包装器已经处于解决状态 // 它永远不会需要改变 () => !instance.isUnmounted && hydrateSubTree()); } else { hydrateSubTree(); } } // 否则就直接执行 patch 函数进行挂载 else { // 挂载的是组件的子树 const subTree = (instance.subTree = renderComponentRoot(instance)); patch(null, subTree, container, anchor, instance, parentSuspense, isSVG); initialVNode.el = subTree.el; } // mounted hook // 执行组件的 mounted 钩子函数 if (m) { queuePostRenderEffect(m, parentSuspense); } // onVnodeMounted // 执行组件的 onVnodeMounted 钩子函数 if (!isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeMounted)) { const scopedInitialVNode = initialVNode; queuePostRenderEffect(() => invokeVNodeHook(vnodeHook, parent, scopedInitialVNode), parentSuspense); } // activated hook for keep-alive roots. // #1742 activated hook must be accessed after first render // since the hook may be injected by a child keep-alive // keep-alive 根的激活钩子 // #1742 激活钩子必须在第一次渲染后访问 // 由于钩子可能由子 keep-alive 注入 if (initialVNode.shapeFlag & 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */ || (parent && isAsyncWrapper(parent.vnode) && parent.vnode.shapeFlag & 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */)) { instance.a && queuePostRenderEffect(instance.a, parentSuspense); } // 设置组件已经挂载 instance.isMounted = true; // #2458: deference mount-only object parameters to prevent memleaks // #2458:延迟挂载仅对象参数以防止内存泄漏 initialVNode = container = anchor = null; } };
可以看到在挂载的过程中,会执行一系列相关的钩子函数,比如 beforeMount
、mounted
、onVnodeBeforeMount
、onVnodeMounted
等等;
这里可能有疑问的是onVnodeBeforeMount
和onVnodeMounted
这两个钩子函数是啥?其实这是Vue
内部使用的钩子函数,我们可以在Vue
的源码中找到它们的定义:
// core/packages/runtime-core/src/vnode.ts 91-113 type VNodeMountHook = (vnode: VNode) => void type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void export type VNodeHook = | VNodeMountHook | VNodeUpdateHook | VNodeMountHook[] | VNodeUpdateHook[] // https://github.com/microsoft/TypeScript/issues/33099 export type VNodeProps = { key?: string | number | symbol ref?: VNodeRef ref_for?: boolean ref_key?: string // vnode hooks onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[] onVnodeMounted?: VNodeMountHook | VNodeMountHook[] onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[] onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[] onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[] onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[] }
至于具体是用于干啥的我并没有找到相关的内容,后续找到了再补充。
这里需要注意的是,挂载最后还是执行的patch
函数,挂载的是组件的子树,这里的子树是通过renderComponentRoot
函数生成的;
总结
这一章主要是介绍了Vue3
的组件挂载过程,其实内容到这里有点仓促结束了,因为这一章的内容比较多,但是通过这一章我们关联了很多东西;
组件的挂载过程中,会使用ReactiveEffect
来执行render
函数,而这个是我们之前花了很多时间来讲的;
同时还是会使用queueJob
来执行updateComponent
函数,这个也是在之前讲过的,可以看看nextTick
的实现那一章;
最后组件挂载是通过patch
函数来完成的,而挂载到最后还是绕回到了patch
函数,后续我们会继续深入的讲解patch
函数的实现;