vue3源码分析(1)-createApp发生了什么?

984 阅读10分钟

开篇导读:

Vue作为一个相当出名的响应式框架。模板语法、声明式、组件化、轻量级、虚拟DOM的优化、组件级更新等都是它的优点,Vue3相比于Vue2更是重写了大部分源码,采用了更严格的Typescript重写,同时采用Proxy代替了Object.defineProperty做响应式劫持使得功能性得到了延展,对于添加属性,减少对象属性等有了更好的处理,同时项目采用monorepo进行管理,使用一个项目管理多个包,并且每个包的功能划分相当明确,每个模块都可以进行单独的单元测试等,并且还加入了组合式Api,对于处理超大组件有了更好的拆分和利用

随着前端发展,仅仅是了解如何使用框架是不够的,对于学习的过程,我认为应该是了解使用,知其然,更要知其所以然,所以阅读源码是相当有必要的,并且现在招聘的要求越来越高,一步一步提升自己是完全正确的。

本系列更多是自己的观点和思考,因为本系列也是边读源码边写的。,如果有错误的地方请指出,喜欢的童鞋记得留下一个👍。

1.Vue源码结构解读

image.png

  • compiler-core 编译的核心包,与运行平台无关。
  • compiler-dom 浏览器平台下的编译器,依赖于compiler-core
  • compiler-ssr 服务端渲染的编译器,同样依赖于compiler-core
  • compiler-sfc 单文件组件的编译器,依赖于compiler-code、compiler-dom
  • reactivity 可单独运行的响应式系统,
  • runtime-core Vue的运行时核心代码,与平台无关
  • runtime-dom Vue在浏览器平台运行的核心代码 好的,大概就是这些,本次我们依赖于vite构建一个vue项目,我们主要讲解vue的运行时,也就是runtime-domruntime-core这两个包。

2.".vue"文件的编译结果

首先我们创建main.ts文件:

import { createApp } from "vue";//引入的runtime-dom
import App from "./App.vue";
//今天主要分析createApp这个函数。
createApp(App).mount("#app");

我们先来App看看编译后的结果吧!

//App.vue
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
</template>

<style scoped>
//省略此处代码...
</style>

//编译后的App.vue
import { defineComponent as _defineComponent } 
from "/node_modules/.vite/deps/vue.js?v=742c4470";
import HelloWorld from "/src/components/HelloWorld.vue";
//编译出的主要对象,这里主要是对setup语法糖进行的编译
//所以虽然你没有写setup函数 那是因为这一步编译器帮你做了
const _sfc_main = _defineComponent({
  __name: "App",
  setup(__props, { expose }) {
    expose();
    //咱们引入的HelloWorld作为返回值返回
    const __returned__ = { HelloWorld };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import {
   //这个就是runtime-core中的createBaseVNode函数
   //通过type props children等属性创建VNode
   //便于后续使用后面会讲到这个方法
   createElementVNode as _createElementVNode, 
   createVNode as _createVNode,
   Fragment as _Fragment, 
   openBlock as _openBlock, 
   createElementBlock as _createElementBlock,
   //全局有一个currentScopeId,用于给当前的作用域Id设置值
   pushScopeId as _pushScopeId,
   //消除这个值
   popScopeId as _popScopeId 
} from "/node_modules/.vite/deps/vue.js?v=742c4470";

//以编译得到的scopeId创建Vnode 创建完成后销毁scopeId
const _withScopeId = (n) => (_pushScopeId("data-v-7a7a37b1"), n = n(), _popScopeId(), n);
//得到template中所有标签组成的Vnode
const _hoisted_1 =  _withScopeId(() =>  _createElementVNode("div", null, [
 _createElementVNode("a", {
    href: "https://vitejs.dev",
    target: "_blank"
  }, [
   _createElementVNode("img", {
      src: "/vite.svg",
      class: "logo",
      alt: "Vite logo"
    })
  ]),
 _createElementVNode("a", {
    href: "https://vuejs.org/",
    target: "_blank"
  }, [
   _createElementVNode("img", {
      src: "/src/assets/vue.svg",
      class: "logo vue",
      alt: "Vue logo"
    })
  ])
], -1));
//渲染函数
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode($setup["HelloWorld"], { msg: "Vite + Vue" })
  ], 64);
}
//对于css的代码 直接编译为import语句导入即可
import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
export const _rerender_only = true;
//这个函数的作用就是将第二个参数的数组中的第一个元素作为key
//第二个元素作为value赋值给第一个参数
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
//{__name:App,setup,render:_src_render,...}
//这就是最终通过编译得到的对象
export default  _export_sfc(
_sfc_main, 
[
 ["render", _sfc_render],
 ["__scopeId", "data-v-7a7a37b1"],
 ["__file", "E:/\u5927\u4E8C\u5B66\u4E60\u6587\u4EF6/23.vue-debugger/src/App.vue"]
 ]
);
//还有一些省略的hmr代码 我们以后在讲解吧!
//defineComponent
function defineComponent(options) {
    //如果是一个函数,包装成一个对象
    return shared.isFunction(options) ? { setup: options, name: options.name } : options;
}
  • 现在我们知道了,我们写的Vue组件会被编译成一个对象,这个对象中包含setup,name(如果你没写,会自动通过读取文件名读取这个值,例如App.vue=>"App" ),以及render函数。看看浏览器中的显示吧!

image.png 知道了import App from "App.vue"这个App到底是什么,我们就开始分析本文的重点createApp吧!

3.分析createApp发生了什么?

下面代码来自node_modules/@vue/runtime-dom,当然我省略了部分不是重点逻辑的代码

//我们暂且认为,createApp传递的参数就是App对象且只有这一个参数
const createApp = 
    (...args) => {
    //获取runtimeCore的渲染器并调用createApp方法
    const app = ensureRenderer().createApp(...args);
    //获取mount方法
    const { mount } = app;
    //重写mount方法这就是咱们后面要讲的挂载流程的起点
    app.mount = (containerOrSelector) => {
        //获取<div id="root"></div>的DOM对象(这个函数处理了字符串和真实DOM两种情况)
        const container = normalizeContainer(containerOrSelector);
        if (!container)
            return;
        const component = app._component;//获取App组件
        if (!shared.isFunction(component) && !component.render && !component.template) {
            component.template = container.innerHTML;
        }
        container.innerHTML = '';
        //执行挂载 arguments(挂载容器,是否是ssr,是否是svg元素)
        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;
};

//ensureRenderer
function ensureRenderer() {
    return (renderer ||
        (renderer = runtimeCore.createRenderer(rendererOptions)));
}
  • 我们可以发现首先调用了ensureRenderer方法,显然这个方法是为了初始化,并且通过runtime-core这个包创建渲染器,同时传入了rendererOptions参数,所以实际上我们最终的app是由runtime-core这个包提供的方法创建的,在分析runtime-core包之前,我们先来看看renderOptions是什么。
const nodeOps = {
    //插入节点
    insert: (child, parent, anchor) => {
        parent.insertBefore(child, anchor || null);
    },
    //移除节点
    remove: child => {
        const parent = child.parentNode;
        if (parent) {
            parent.removeChild(child);
        }
    },
    //创建Html元素
    createElement: (tag, isSVG, is, props) => {
        const el = isSVG
            ? doc.createElementNS(svgNS, tag)
            : doc.createElement(tag, is ? { is } : undefined);
        if (tag === 'select' && props && props.multiple != null) {
            el.setAttribute('multiple', props.multiple);
        }
        return el;
    },
    //创建文本节点
    createText: text => doc.createTextNode(text),
    //创建注释节点
    createComment: text => doc.createComment(text),
    setText: (node, text) => {
        node.nodeValue = text;
    },
    //设置元素的内容
    setElementText: (el, text) => {
        el.textContent = text;
    },
    //获取元素的父元素
    parentNode: node => node.parentNode,
    //获取下一个兄弟元素
    nextSibling: node => node.nextSibling,
    //使用选择器获取元素
    querySelector: selector => doc.querySelector(selector),
    setScopeId(el, id) {
        el.setAttribute(id, '');
    },
    //克隆节点
    cloneNode(el) {
        const cloned = el.cloneNode(true);
        if (`_value` in el) {
            cloned._value = el._value;
        }
        return cloned;
    },
    //插入静态内容
    insertStaticContent(content, parent, anchor, isSVG, start, end) {
       //省略部分代码...
    }
};
//Object.assgin()
const rendererOptions = shared.extend({ patchProp }, nodeOps);
  • renderOptions实际上就是一些浏览器的Api,传递给runtime-core,便于runtime-core内部执行使用的。 接下来我们来到runtime-core包的createRenderer函数。
function createRenderer(options) {
    //创建基础的渲染器
    return baseCreateRenderer(options);
}

//创建渲染器
function baseCreateRenderer(options, createHydrationFns) {
    const target = shared.getGlobalThis();//获取全局对象
    target.__VUE__ = true;
    //重options中解构方法
    const { insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = shared.NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent } = options;
    const patch = ()=>{}
    const processText = ()=>{}
    const processCommentNode = ()=>{}
    const mountStaticNode = ()=>{}
    const patchStaticNode =()=>{}
    const moveStaticNode = ()=>{}
    const removeStaticNode = ()=>{}
    const processElement = ()=>{}
    const mountElement = ()=>{}
    const setScopeId = ()=>{}
    const patchElement = ()=>{}
    const patchProps = ()=>{}
    const processFragment = ()=>{}
    const mountComponent = ()=>{}
    const updateComponent = ()=>{}
    const setupRenderEffect = ()=>{}
    const patchChildren = ()=>{}
    const patchUnkeyedChildren =()=>{}
    const patchKeyedChildren =()=>{}
    const move = ()=>{}
    const unmount = ()=>{}
    const remove = ()=>{}
    const removeFragment = ()=>{}
    const unmountComponent = ()=>{}
    const getNextHostNode = ()=>{}
    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;
    };
    return {
        render,
        //主要逻辑
        createApp: createAppAPI(render, hydrate)
    };
}
  • 这个函数咋一看相当的复杂,我们可以这些函数的函数名读出大概的意思,他们都是用于挂载,比较,卸载的方法,显然,这就是这个渲染器最核心的功能了,但是这一段代码非常长,我们以后在慢慢来解读,现在我们只需要知道createRenderer返回了一个对象主要有rendercreateApp方法(还有一个hydrate方法,用于服务端渲染我们不关注这部分逻辑)。createApp的值是通过createAppAPI获得的,所以我们接下来看看这个函数的返回值就可以了!
function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        //import App from "App.vue"
        //判断当前引入的App是否是一个函数,目前应该是一个对现象
        if (!shared.isFunction(rootComponent)) {
            //浅克隆
            rootComponent = { ...rootComponent };
        }
        if (rootProps != null && !shared.isObject(rootProps)) {
            warn(`root props passed to app.mount() must be an object.`);
            rootProps = null;
        }
        const context = createAppContext();//创建App上下文
        const installedPlugins = new Set();//已经安装过的插件
        let isMounted = false;//是否挂载过
        //main.ts中createApp创造出来的app
        const app = (context.app = {
            _uid: uid++,//标识符
            _component: rootComponent,//App组件
            _props: rootProps,//根props
            _container: null,//挂载容器
            _context: context,//刚才创建的上下文
            _instance: null,//根组件的实例
            version,//当前vue的版本
            //config的getter
            get config() {
                return context.config;
            },
            //setter
            set config(v) {
                {
                    warn(`app.config cannot be replaced. Modify individual options instead.`);
                }
            },
            //注入插件的方法
            use(plugin, ...options) {
                //已经安装过这个插件了提示用户
                if (installedPlugins.has(plugin)) {
                    warn(`Plugin has already been applied to target app.`);
                }
                //所有的插件应当具有install方法,参数为app全局对象以及传入的options
                else if (plugin && shared.isFunction(plugin.install)) {
                    installedPlugins.add(plugin);//添加到集合中防止重复应用插件
                    plugin.install(app, ...options);//执行插件的install方法
                }
                //如果插件是一个函数,执行
                else if (shared.isFunction(plugin)) {
                    installedPlugins.add(plugin);
                    plugin(app, ...options);
                }
                else {
                    //warn提示
                }
                return app;
            },
            mixin(mixin) {
                //省略这部分代码...
            },
            component(name, component) {
                //省略这部分代码...
            },
            directive(name, directive) {
                //省略这部分代码...
            },
            mount(rootContainer, isHydrate, isSVG) {
             if (!isMounted) {
                //这里表示已经挂载过了
                if (rootContainer.__vue_app__) {
                    //warn提示
                }
                //创建Vnode 
                const vnode = createVNode(rootComponent, rootProps);
                vnode.appContext = context;
                //通过Vnode渲染节点
                render(vnode, rootContainer, isSVG);
                isMounted = true;
                app._container = rootContainer;
                rootContainer.__vue_app__ = app;
                app._instance = vnode.component;
            }
            else {
              //warn提示   
             }
            },
            
            unmount() {
             //如果已经挂载过了卸载之前的
             if(isMounted) {
               render(null, app._container);
                delete app._container.__vue_app__;
              }
              else {
                warn(`Cannot unmount an app that is not mounted.`);
              }
            },
            provide(key, value) {
                //省略代码...
            }
        });
        return app;
    };
}
  • 到了这里我们终于是看到了app的真面目,他本质就是一个包含了许多信息和方法的对象,其中就用挂载的方法mount注册插件的方法app.use(),我们也在这里看到了插件的格式必须是一个对象带有install方法或则是一个函数,而其他例如mixin,component等方法,都是将传入的参数缓存到了app.context中,我们就不展开讲了。app中比较重要的属性我已经在注释中给出了,接下来我们看看app.context属性吧!
//大家知道有这么个对象就行了,后面我们在慢慢讲他的作用
function createAppContext() {
    return {
        app: null,//createApp返回的Vue对象
        config: {
            isNativeTag: shared.NO,//是否是原生tag
            performance: false,
            globalProperties: {},
            optionMergeStrategies: {},
            errorHandler: undefined,
            warnHandler: undefined,
            compilerOptions: {}
        },
        mixins: [],//缓存mixin的地方
        components: {},//缓存componen
        directives: {},//缓存directive
        provides: Object.create(null),
        optionsCache: new WeakMap(),
        propsCache: new WeakMap(),
        emitsCache: new WeakMap()
    };
}

好啦,到这里我们的app就已经完成了创建了,我们最后看看创建出来的app结构吧!

component: ƒ component(name, component)
config: Object /context.config
directive: ƒ directive(name, directive)
mixin: ƒ mixin(mixin)
mount: (containerOrSelector) => {…}
provide: ƒ provide(key, value)
unmount: ƒ unmount()//卸载的方法
use: ƒ use(plugin, ...options)//使用插件
version: "3.2.39"//vue的版本
//根组件App的编译后结果
_component: {__name'App'__scopeId'data-v-7a7a37b1'setup: ƒ, render: ƒ, …}
_container: null//挂载的容器 mount阶段才会赋值
_context: {app: {…}, config: {…}...}
_instance: null//组件实例
_props: null
_uid: 0//标识符id

4.本文总结:

  • 首先我们简单介绍了Vue3的更新以及优点
  • 然后我们分析了App.vue编译后的文件,了解到了 ".vue" 文件最终会被编译成一个包含有name,render,setup等信息和方法的对象。
  • 在然后我们通过createApp为入口一步一步分析,最终发现了app的来源,首先调用了runtime-dom中的ensureRenderer方法,然后这个方法会调用runtime-core中的createRenderer并且传入renderOptions,这个renderOptions就是包含一些浏览器操作dom的方法,在createRenderer写了多达二十几个方法一千多行代码,最终返回了一个对象 {render,createApp} ,然后再调用这个对象中的createApp方法创建出了app对象,同时这个对象包含了mount方法,也就是挂载流程的起点。我们下一节再继续进行分析吧!