vue3组件渲染:从入口开始

一个项目足够复杂的话,所有代码如果都在一个页面中,那么,就会出现一个文件上万行代码的可能。vue通过组件化,将页面按照模块或功能进行拆分,方便团队合作和后期维护。组件化让项目开发如同搭积木一样简单,借用官方图示如下:

那么,组件化是如何实现的呢?

这还得从入口说起 ...

javascript 复制代码
// main.js文件
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);
app.mount('#app');

例子中可以看出,通过import { createApp } from 'vue'的方式从vue中引入了方法createApp,并将App作为参数传入其中,最后,通过app.mount('#app')的方式将app挂载到#app上去。

一、const app = createApp(App)

javascript 复制代码
const createApp = ((...args) => {
    // 创建app方法
    const app = ensureRenderer().createApp(...args);
    // 从app中获取mount方法
    const { mount } = app;
    // 重写app.mount方法
    app.mount = (containerOrSelector) => {
        // ...
    };
    return app;
});

createApp的主要逻辑可以分为获取app和重写app.mount

1、创建app

创建app需要通过const app = ensureRenderer().createApp(...args)的方式,这里可以将流程分为两步,ensureRenderercreateApp

(1)ensureRenderer

javascript 复制代码
const rendererOptions = extend({ patchProp }, nodeOps);
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer;
function ensureRenderer() {
    return (renderer ||
        (renderer = createRenderer(rendererOptions)));
}
function createRenderer(options) {
    return baseCreateRenderer(options);
}
function baseCreateRenderer(options, createHydrationFns) {
    // ...
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    };
}
function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        // ...
        return app;
    };
}

这里的rendererOptions是合并而成的操作dom的一系列方法,并通过return (renderer || (renderer = createRenderer(rendererOptions)))的方式对renderer做缓存处理,如果存在直接返回,如果不存在,才进行后续的操作,这也是一种优化的策略。

baseCreateRenderer(options)的方式返回了一个包含createApp: createAppAPI(render, hydrate)的对象,其最终返回的是函数function createApp(rootComponent, rootProps = null){...}

所以const app = ensureRenderer().createApp(...args)最终执行的就是createAppAPI内部返回的createApp函数。

(2)createApp

javascript 复制代码
function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
        rootComponent = Object.assign({}, rootComponent);
    }
    // ...
    const context = createAppContext();
    // ...
    const app = (context.app = {
        _uid: uid++,
        _component: rootComponent,
        _props: rootProps,
        _container: null,
        _context: context,
        _instance: null,
        version,
        mount() {
            // 与平台无关的mount方法
        },
        // 还有其他方法
    });
    return app;
}
function createAppContext() {
    return {
        app: null,
        config: {
            isNativeTag: NO,
            performance: false,
            globalProperties: {},
            optionMergeStrategies: {},
            errorHandler: undefined,
            warnHandler: undefined,
            compilerOptions: {}
        },
        mixins: [],
        components: {},
        directives: {},
        provides: Object.create(null),
        optionsCache: new WeakMap(),
        propsCache: new WeakMap(),
        emitsCache: new WeakMap()
    };
}

如果rootComponent不是函数,通过rootComponent = Object.assign({}, rootComponent)的方式浅拷贝rootComponent,再通过createAppContext的方式返回一个app执行的环境。最终返回一个包含_uid_component_props_container_context_instanceversionmount等属性的app对象,这里的mount与平台无关。

2、缓存mount

先通过const { mount } = app的方式将mount方法从app中拿出来缓存备用。

3、重写app.mount

然后通过app.mount = containerOrSelector) => {...}的方式对app.mount方法进行重写。

这样做的目的是,将与平台无关的mount进行缓存,然后在不同的平台中重写app.mout方法进行特定场景的处理。最终还是会执行到与平台无关的mount函数。

javascript 复制代码
// 重写的app.mount
app.mount = (containerOrSelector) => {
    const container = normalizeContainer(containerOrSelector);
    if (!container)
        return;
    const component = app._component;
    if (!isFunction(component) && !component.render && !component.template) {
        // __UNSAFE__
        // Reason: potential execution of JS expressions in in-DOM template.
        // The user must make sure the in-DOM template is trusted. If it's
        // rendered by the server, the template should not contain any user data.
        component.template = container.innerHTML;
    }
    // clear content before mounting
    container.innerHTML = '';
    const proxy = mount(container, false, container instanceof SVGElement);
    if (container instanceof Element) {
        container.removeAttribute('v-cloak');
        container.setAttribute('data-v-app', '');
    }
    return proxy;
};

其中const proxy = mount(container, false, container instanceof SVGElement);执行的就是前面缓存的mount

二、app.mount('#app')

当执行到入口文件的app.mount('#app')时,就会执行重写的方法app.mount

这里先通过const container = normalizeContainer(containerOrSelector)的方式去将非 DOM 容器的字符串转换成 DOM 节点,内部使用了 DOM 操作的原生方法document.querySelector(container)

再通过const proxy = mount(container, false, container instanceof SVGElement)的方式去调用与平台无关的mount方法,下面详细介绍mount相关的逻辑:

scss 复制代码
// 通过const { mount } = app获取到的mount
mount(rootContainer, isHydrate, isSVG) {
    if (!isMounted) {
        // #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.`);
        }
        const vnode = createVNode(rootComponent, rootProps);
        // store app context on the root VNode.
        // this will be set on the root instance on initial mount.
        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);
        } else {
            render(vnode, rootContainer, isSVG);
        }
        isMounted = true;
        app._container = rootContainer;
        rootContainer.__vue_app__ = app;
        if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
            app._instance = vnode.component;
            devtoolsInitApp(app, version);
        }
        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)\``);
    }
}

1、生成vNode

这里通过const vnode = createVNode(rootComponent, rootProps)的方式生成vnode,当前例子中会执行到以下逻辑:

go 复制代码
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
    ? 1 /* ShapeFlags.ELEMENT */
    : isSuspense(type)
        ? 128 /* ShapeFlags.SUSPENSE */
        : isTeleport(type)
            ? 64 /* ShapeFlags.TELEPORT */
            : isObject(type)
                ? 4 /* ShapeFlags.STATEFUL_COMPONENT */
                : isFunction(type)
                    ? 2 /* ShapeFlags.FUNCTIONAL_COMPONENT */
                    : 0;
return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true)

其中通过判断组件被编译后的type,来确定shapeFlag,当前例子中其值为4,再继续看createBaseVNode

yaml 复制代码
function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ShapeFlags.ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) {
    const vnode = {
        __v_isVNode: true,
        __v_skip: true,
        type,
        props,
        key: props && normalizeKey(props),
        ref: props && normalizeRef(props),
        scopeId: currentScopeId,
        slotScopeIds: null,
        children,
        component: null,
        suspense: null,
        ssContent: null,
        ssFallback: null,
        dirs: null,
        transition: null,
        el: null,
        anchor: null,
        target: null,
        targetAnchor: null,
        staticCount: 0,
        shapeFlag,
        patchFlag,
        dynamicProps,
        dynamicChildren: null,
        appContext: null,
        ctx: currentRenderingInstance
    };
    // ...
    return vnode;
}

可以看出,当前的vnode就是一个由许多属性组成的对象,用来描述当前组件的主要信息,如同 DOM 树用来描述页面html一样。

紧接着会通过vnode.appContext = context的方式为vnode.context进行赋值。从文中刚开始可以看出,context是在createApp方法中通过context = createAppContext()的方式定义的,该方法中也为context.app进行了赋值。

生成vnode以后就会进行vnode的渲染逻辑,最终其实也是调用了 DOM 操作的原生 API,继续往下看。

2、vNode渲染

通过render(vnode, rootContainer, isSVG):

ini 复制代码
const render = (vnode, container, isSVG) => {
    if (vnode == null) {
        if (container._vnode) {
            unmount(container._vnode, null, null, true);
        }
    }
    else {
        patch(container._vnode || null, vnode, container, null, null, null, isSVG);
    }
    flushPreFlushCbs();
    flushPostFlushCbs();
    container._vnode = vnode;
};

当前例子中vnode存在,所以会执行到patch的逻辑,其中有主要的逻辑如下:

scss 复制代码
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);
}

当前例子中首次渲染执行时shapeFlag4,满足shapeFlag & 6为真值,所以会执行到processComponent:

ini 复制代码
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    n2.slotScopeIds = slotScopeIds;
    if (n1 == null) {
        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);
    }
};

当前例子中旧的节点vnodenull,并且n2.shapeFlag & 5120,所以会执行到mountComponent的逻辑:

scss 复制代码
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
    const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
    if ((process.env.NODE_ENV !== 'production') && instance.type.__hmrId) {
        registerHMR(instance);
    }
    if ((process.env.NODE_ENV !== 'production')) {
        pushWarningContext(initialVNode);
        startMeasure(instance, `mount`);
    }
    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
        instance.ctx.renderer = internals;
    }
    // resolve props and slots for setup context
    {
        if ((process.env.NODE_ENV !== 'production')) {
            startMeasure(instance, `init`);
        }
        setupComponent(instance);
        if ((process.env.NODE_ENV !== 'production')) {
            endMeasure(instance, `init`);
        }
    }
    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (instance.asyncDep) {
        parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
        // Give it a placeholder if this is not hydration
        // TODO handle self-defined fallback
        if (!initialVNode.el) {
            const placeholder = (instance.subTree = createVNode(Comment));
            processCommentNode(null, placeholder, container, anchor);
        }
        return;
    }
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
    if ((process.env.NODE_ENV !== 'production')) {
        popWarningContext();
        endMeasure(instance, `mount`);
    }
};

以上有两个重要的逻辑,创建instance和调用setupRenderEffect

(1)instance

在当前逻辑中,通过const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))的方式创建子组件实例:

ini 复制代码
function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    // inherit parent app context - or - if root, adopt from root vnode
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        uid: uid$1++,
        vnode,
        type,
        parent,
        appContext,
        root: null,
        next: null,
        subTree: null,
        // 还有好多其他属性
    };
    if ((process.env.NODE_ENV !== 'production')) {
        instance.ctx = createDevRenderContext(instance);
    } else {
        instance.ctx = { _: instance };
    }
    instance.root = parent ? parent.root : instance;
    // ...
    return instance;
}

当前逻辑中通过const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext的方式去获取appContext,有父组件拿父组件的,无父组件拿当前vnode的,如果都找不到则使用默认的emptyAppContext

在线上环境通过instance.ctx = { _: instance }的方式为instance定义ctx,其实就是它本身。

再通过instance.root = parent ? parent.root : instance的方式为instance定义根实例root

(2)setupComponent

ini 复制代码
function setupComponent(instance, isSSR = false) {
    isInSSRComponentSetup = isSSR;
    const { props, children } = instance.vnode;
    const isStateful = isStatefulComponent(instance);
    initProps(instance, props, isStateful, isSSR);
    initSlots(instance, children);
    const setupResult = isStateful
        ? setupStatefulComponent(instance, isSSR)
        : undefined;
    isInSSRComponentSetup = false;
    return setupResult;
}

这里会处理propsslots,当前例子中不涉及,暂时不介绍。

(3)setupRenderEffect

ini 复制代码
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
    const componentUpdateFn = () => {
        // 最后调用update()的时候,会执行到这里
    };
    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance
        .scope // track it in component's effect scope
    ));
    const update = (instance.update = () => effect.run());
    update.id = instance.uid;
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    toggleRecurse(instance, true);
    if ((process.env.NODE_ENV !== 'production')) {
        effect.onTrack = instance.rtc ?
            e => invokeArrayFns(instance.rtc, e) :
            void 0;
        effect.onTrigger = instance.rtg ?
            e => invokeArrayFns(instance.rtg, e) :
            void 0;
        update.ownerInstance = instance;
    }
    update();
}; 

通过new ReactiveEffect的方式创建ReactiveEffect实例,并赋值给instance.effect。通过const update = (instance.update = () => effect.run())的方式为instance.update赋值调用effect.run()的函数。最后,执行到update(),最终会执行componentUpdateFn

componentUpdateFn函数中有两个重点:获取subTree和渲染subTree

subTree的获取

通过const subTree = (instance.subTree = renderComponentRoot(instance))的方式获取subTree

renderComponentRoot中主要的逻辑为:

ini 复制代码
result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx))

此时的render函数为:

php 复制代码
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
    const _component_child = _resolveComponent("child")

    return (_openBlock(), _createElementBlock("div", null, [
        _hoisted_1,
        _createVNode(_component_child)
    ]))
}

最终的执行结果为描述<h3>这个是app组件</h3><child></child>vnode

javascript 复制代码
[
    {
        type: 'h3',
        children: "这个是app组件"
    }, {
        children: null,
        type: {
            render: function _sfc_render(_ctx, _cache) {
                return (_openBlock(), _createElementBlock("p", null, "这个是child组件"))
            }
        }
    }
]

从结果中可以看出,第一个元素是普通的h3节点。第二个元素是有render函数的组件节点。

subTreepatch

通过patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)的方式渲染subTree

patch函数中,shapeFlag17shapeFlag & 1为真值1,所以会执行到processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)

processElement中满足n1 == null,执行到mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)

mountElementshapeFlag & 16为真值16。会执行到mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized):

ini 复制代码
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, 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, isSVG, slotScopeIds, optimized);
    }
};

这里分别先看h3的渲染:

scss 复制代码
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds,
    optimized) => {
    // 根据tag创建dom
    el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);
    // 根据vnode.children为text节点赋值
    if (shapeFlag & 8 /* ShapeFlags.TEXT_CHILDREN */ ) {
        hostSetElementText(el, vnode.children);
    }
    // 将文本节点插入到父节点中
    hostInsert(el, container, anchor);
};

再看看child的渲染:

我们发现child是组件节点,然后会执行到patch中的processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized),最后执行到mountComponent逻辑。又回到了mountComponent,递归开始。

执行到const subTree = (instance.subTree = renderComponentRoot(instance))的方式获取subTree 后,简单看最终的执行结果为描述<child></child>vnode

bash 复制代码
{
    type: 'p',
    children: "这个是child组件"
}

然后在子组件child的渲染过程中,会依然执行hostCreateElementhostSetElementTexthostInsert的逻辑,最终将真实节点插入到父节点中。

执行完以后,跳出到上一级mountChildren逻辑中,将当前获取到的el通过hostInsert(el, container, anchor)的方式插入到父节点中,此时父节点中的节点为<div><h3>这个是app组件</h3><p>这个是child组件</p></div>

页面渲染至此完成,简单总结如下:

mountElement的过程中,如果遇到mountChildren渲染过程子组件列表,普通节点会通过mountElement进行普通节点的创建和插入,组件节点会递归的执行processComponent将子组件树subTreeel插入到父节点中。这样,普通节点的el,子组件树中的el,都插入到了父节点中。依次类推,通过先子后父的方式,一层层的将节点插入到根节点中。

总结

vue组件树的渲染,是一个深度遍历的过程,从根节点开始寻找可创建真实节点的叶子节点,叶子节点完成真实节点的渲染后,再将其el交给父组件。依次类推,叶子节点将其el交给上一次中间组件,中间组件沿着树交给父级组件,最终会交给根组件。

纰漏之处在所难免,请批评指正。

相关推荐
栈老师不回家1 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙7 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠11 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds31 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果1 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长1 小时前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
阿伟来咯~1 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app