【源码&库】 Vue3 的组件是如何挂载的?

简介: 【源码&库】 Vue3 的组件是如何挂载的?

从我最开始讲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语句中一共有八种情况:


  1. 文本节点
  2. 注释节点
  3. 静态节点
  4. Fragment 节点
  5. 元素节点
  6. 组件节点
  7. Teleport 组件包裹的节点
  8. Suspense 组件包裹的节点


这其中TeleportSuspense组件包裹的节点,我们暂时不用关注,因为这两个组件还没有讲到,我们先来看看前面的几种情况。


文本节点的挂载


文本节点挂载是通过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的实现还是比较简单的,这里主要关心的是hostInserthostCreateTexthostSetText这三个函数。


但是这三个函数是在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);
    }

mountStaticNodepatchStaticNode函数的代码如下:

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.eln2.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);
};

上面的代码移除了开发环境下的一些代码,后面的代码讲解也会默认去掉开发环境下的代码;


上面的代码其实并不多,抛开边界情况,只执行了三个函数,createComponentInstancesetupComponentsetupRenderEffect


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函数的作用是初始化组件的 propsslots,然后调用 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转换为 propsattrs,然后通过 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进行初始化,如果 vnodeshapeFlag32,则说明定义了 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函数;


代码中的配置项,例如:isCustomElementcompilerOptionsdelimiterscompilerOptions等,可以在官网中查到,这里不提供链接了,大家可以自行查阅。


到这里,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;
    }
};

可以看到在挂载的过程中,会执行一系列相关的钩子函数,比如 beforeMountmountedonVnodeBeforeMountonVnodeMounted等等;


这里可能有疑问的是onVnodeBeforeMountonVnodeMounted这两个钩子函数是啥?其实这是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函数的实现;



目录
相关文章
|
4天前
|
JavaScript
vue3自定义指令
vue3自定义指令
|
4天前
Vue3中怎么监听store中的数据变化
Vue3中怎么监听store中的数据变化
|
4天前
|
JavaScript
vue3中reactive和ref函数及对比
vue3中reactive和ref函数及对比
|
1天前
|
JavaScript 前端开发 编译器
Vue组件(Component)
Vue组件(Component)
16 3
|
1天前
|
存储 JavaScript
在 Vue 中如何进行组件间的通信?
在 Vue 中如何进行组件间的通信?
8 1
|
2天前
|
JavaScript 前端开发 安全
轻松创建自定义手势图案锁 - Vue 手势密码锁组件
轻松创建自定义手势图案锁 - Vue 手势密码锁组件
30 0
|
3天前
|
JavaScript 前端开发 Java
花了一天时间, 搜刮的个人觉得不错的16套vue3后台管理模版
花了一天时间, 搜刮的个人觉得不错的16套vue3后台管理模版
30 2
花了一天时间, 搜刮的个人觉得不错的16套vue3后台管理模版
|
4天前
|
JavaScript
vue3表格编辑(数据回显)和删除功能实现
vue3表格编辑(数据回显)和删除功能实现
|
4天前
Vue3中props的原理与使用
Vue3中props的原理与使用
|
4天前
|
存储 JavaScript 前端开发
在Vue中,除了`beforeDestroy`生命周期钩子函数,还有哪些方法可以在组件销毁时清理定时器?
在Vue中,除了`beforeDestroy`生命周期钩子函数,还有哪些方法可以在组件销毁时清理定时器?
9 2

相关产品

  • 云迁移中心