Vue 3 源码学习之路-01-源码结构&createApp

47,705 阅读4分钟

Version: 3.0.11

1.准备工作-源码结构

1.1目录结构

在正式学习源码之前,首先在Vue官方 github 官网下载源码 ,下载之后解压目录大概是这样的。

vue-next.png

  • compiler-core

compiler-core.png

  • compiler-dom

compiler-dom.png

  • compiler-sfc

compiler-sfc.png

  • compiler-ssr

compiler-ssr.png

  • reactivity

reactivity.png

  • runtime-core 运行时核心 先上一张图来看看runtime-core里面有啥东西。

runtime-core.png

通过上图我们不难看出Vue的主要的核心api都在运行时核心里面,而且Vue3全部使用TypeScript作为开发语言。

在runtime-core中先说说主要的几个模块vnode、h、components、apiCreateApp、apiLifecycle,因为这个模块是实现Vue3 compositionApi功能的核心

  • runtime-dom

runtime-dom.png

  • server-render

server-render.png

  • sfc-playground

sfc-playground.png

  • shared

shared.png

  • size-check
  • template-explorer
  • vue

vue.png

1.2 源码编译后的样子

大概就长成这个样子!不难发现每个功能模块有3个js文件和一个ts,首先说js文件,在平时使用dev模式开发的时候调用的是带有esm-bundler版本,项目打包 build的时候调用的是prod版本,剩余的一个版本暂时不清楚什么时候调用,ts文件里面定义了接口类型,并非真正意义上的运行代码,辅助开发的时候数据类型推断。

vue-next-core01.png

vue-next-core02.jpg

2 createApp做了什么

2.1 从main.js入手createApp

首先我们看一下Vue3初始化代码,vue3初始化是通过执行createApp方法创建vue实例,而在vue2中使用new Vue()创建。那么下面我们来看一下createApp做了什么。

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

通过浏览器devtool断点执行我们看到createApp方法里面,先执行下面这段代码。

这里主要干了三件事,第一创建renderer,第二创建app实例,第三重写mount函数。 首先通过ensureRenderer方法创建renderer渲染器,然后再通过createApp创建app实例,最后重写app.mount方法,返回proxy代理组件实例

const createApp = ((...args) => {
    const app = ensureRenderer().createApp(...args);
    if ((process.env.NODE_ENV !== 'production')) {
        injectNativeTagCheck(app);
        injectCustomElementCheck(app);
    }
    const { mount } = app;
    app.mount = (containerOrSelector) => {
        const container = normalizeContainer(containerOrSelector);
        if (!container)
            return;
        const component = app._component;
        if (!isFunction(component) && !component.render && !component.template) {
            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;
    };
    return app;
});

2.2 创建renderer

那么看到第一个执行的方法是ensureRender(),通过源码注释了解到惰性创建渲染器renderer,这样做的原因是用户通过Vue只引入了reactivity时可以保证tree-shaking可用 如果renderer已经创建那么直接返回已经创建好的renderer,否则通过createRenderer创建新的渲染器renderer。


const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer;
let enabledHydration = false;
function ensureRenderer() {
    return renderer || (renderer = createRenderer(rendererOptions));
}

createRenderer方法接收一个rendererOption参数,其中nodeOps里面封装的方法就是dom节点操作,当template模板数据变化的时候调用。

patchProp更新dom属性,包括style内敛样式,class类样式,以及patchEvent事件绑定

再看createRenderer方法,内执行baseCreateRenderer方法并将option参数传入。


function createRenderer(options) {
    console.log('%c调用 baseCreateRenderer >>>','color:red')
    return baseCreateRenderer(options);
}

接着再看baseCreateRenderer方法,内部函数众多代码长先跳过,调用baseCreateRenderer方法是返回一个对象,包含三个元素:render,hydrate和createApp。

var count = 0
function baseCreateRenderer(options, createHydrationFns) {
    

    // 代码过长省略N行 ...
   
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    };
}

2.3 创建app实例

在返回参数里面看到调用了createAppAPI方法,也就是说在我们main.js里面执行createApp实际上调用的就是createAppAPI,返回一个app对象,我们再通过createAppAPI看app对象具体有哪些参数

function createAppContext() {
    return {
        app: null,
        config: {
            isNativeTag: NO,
            performance: false,
            globalProperties: {},
            optionMergeStrategies: {},
            isCustomElement: NO,
            errorHandler: undefined,
            warnHandler: undefined
        },
        mixins: [],
        components: {},
        directives: {},
        provides: Object.create(null)
    };
}

let uid = 0;
function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        if (rootProps != null && !isObject(rootProps)) {
            (process.env.NODE_ENV !== 'production') && warn(`root props passed to app.mount() must be an object.`);
            rootProps = null;
        }
        const context = createAppContext();
        const installedPlugins = new Set();
        let isMounted = false;
        const app = (context.app = {
            _uid: uid++,
            _component: rootComponent,
            _props: rootProps,
            _container: null,
            _context: context,
            version,
            get config() {
                return context.config;
            },
            set config(v) {
                if ((process.env.NODE_ENV !== 'production')) {
                    warn(`app.config cannot be replaced. Modify individual options instead.`);
                }
            },
            use(plugin, ...options) {
                if (installedPlugins.has(plugin)) {
                    (process.env.NODE_ENV !== 'production') && warn(`Plugin has already been applied to target app.`);
                }
                else if (plugin && isFunction(plugin.install)) {
                    installedPlugins.add(plugin);
                    plugin.install(app, ...options);
                }
                else if (isFunction(plugin)) {
                    installedPlugins.add(plugin);
                    plugin(app, ...options);
                }
                else if ((process.env.NODE_ENV !== 'production')) {
                    warn(`A plugin must either be a function or an object with an "install" ` +
                        `function.`);
                }
                return app;
            },
            mixin(mixin) {
                if (__VUE_OPTIONS_API__) {
                    if (!context.mixins.includes(mixin)) {
                        context.mixins.push(mixin);
                        // global mixin with props/emits de-optimizes props/emits
                        // normalization caching.
                        if (mixin.props || mixin.emits) {
                            context.deopt = true;
                        }
                    }
                    else if ((process.env.NODE_ENV !== 'production')) {
                        warn('Mixin has already been applied to target app' +
                            (mixin.name ? `: ${mixin.name}` : ''));
                    }
                }
                else if ((process.env.NODE_ENV !== 'production')) {
                    warn('Mixins are only available in builds supporting Options API');
                }
                return app;
            },
            component(name, component) {
                if ((process.env.NODE_ENV !== 'production')) {
                    validateComponentName(name, context.config);
                }
                if (!component) {
                    return context.components[name];
                }
                if ((process.env.NODE_ENV !== 'production') && context.components[name]) {
                    warn(`Component "${name}" has already been registered in target app.`);
                }
                context.components[name] = component;
                return app;
            },
            directive(name, directive) {
                if ((process.env.NODE_ENV !== 'production')) {
                    validateDirectiveName(name);
                }
                if (!directive) {
                    return context.directives[name];
                }
                if ((process.env.NODE_ENV !== 'production') && context.directives[name]) {
                    warn(`Directive "${name}" has already been registered in target app.`);
                }
                context.directives[name] = directive;
                return app;
            },
            mount(rootContainer, isHydrate, isSVG) {
                if (!isMounted) {
                    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__) {
                        devtoolsInitApp(app, version);
                    }
                    return 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)\``);
                }
            },
            unmount() {
                if (isMounted) {
                    render(null, app._container);
                    if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
                        devtoolsUnmountApp(app);
                    }
                    delete app._container.__vue_app__;
                }
                else if ((process.env.NODE_ENV !== 'production')) {
                    warn(`Cannot unmount an app that is not mounted.`);
                }
            },
            provide(key, value) {
                if ((process.env.NODE_ENV !== 'production') && key in context.provides) {
                    warn(`App already provides property with key "${String(key)}". ` +
                        `It will be overwritten with the new value.`);
                }
                // TypeScript doesn't allow symbols as index type
                // https://github.com/Microsoft/TypeScript/issues/24587
                context.provides[key] = value;
                return app;
            }
        });
        return app;
    };
}

createAppAPI的核心就是调用createAppContext 返回一个包含app、config、mixins、components、directives和provides基础参数对象, 然后再创建context.app对象,也就是vue的实例对象,包含内部参数和版本号,暴露出use、component、mount、mixin、directive、unmount、provide程序api给我们使用。

本文作者:自如大前端研发中心-贾文莉