在应用Vue3时,咱们须要应用createApp来创立一个利用实例,而后应用mount办法将利用挂载到某个DOM节点上。

那么在调用createApp时,Vue再背地做了些什么事件呢?明天就来扒一扒Vue3的源码,看看调用createApp产生了些什么。

大家好,这里是田八的【源码&库】系列,Vue3的源码浏览打算,Vue3的源码浏览打算不出意外每周一更,欢送大家关注。

首发在掘金,如果想一起交换的话,能够点击这里一起独特交换成长

系列章节:

  • 【源码&库】跟着 Vue3 学习前端模块化

寻找入口

在上一章中,咱们咱们曾经将Vue3的源码下载下来了,并且曾经晓得如何编译源码了,先看一下Vue3的源码目录:

packages目录下的包就是Vue3的所有源码了,编译之后会在每个工程包上面生成一个dist目录,外面就是编译后的文件。

这里我框出了vue包,这个大家都相熟,关上vue包下的package.json文件,能够看到unpkg字段指向了dist/vue.global.js文件,这个文件就是Vue3的全局版本,咱们能够间接在浏览器中引入这个文件来应用Vue3

代码逻辑基本上都是雷同的,用打包后的文件来剖析源码,能够更加直观的看到源码的逻辑,因为Vue在设计的时候会思考其余平台,如果间接通过源码来查看会有额定的心智累赘。

具体如何应用每个打包后的文件,能够查看vue包下的README.md文件,如果只是想剖析源码,且不想那么麻烦,能够间接应用dist/vue.global.js文件。

如果想理解Vue3的目录构造和模块划分能够应用vue.esm-bundler.js文件,这个文件是Vue3ESM版本,会通过import来引入其余模块,这样就能够间接看到Vue3的模块划分。

本系列就会通过vue.esm-bundler.js文件来剖析Vue3的源码,并且会通过边剖析边入手的形式来学习Vue3的源码。

应用

咱们先来看一下Vue3的应用形式:

import {createApp} from 'vue'import App from './App.vue'const app = createApp(App)app.mount('#app')

Vue3中,咱们须要应用createApp来创立一个利用实例,而后应用mount办法将利用挂载到某个DOM节点上。

createApp是从vue包中导出的一个办法,它接管一个组件作为参数,而后返回一个利用实例。

入口 createApp

vuepackage.json能够看到,module字段指向了dist/vue.esm-bundler.js文件,这个文件是Vue3ESM版本,咱们能够间接应用import来引入Vue3

createApp办法并不在这个包中,而是在runtime-dom包中,这个文件是间接全副导出runtime-dom包中的内容:

export * from '@vue/runtime-dom';

不必狐疑@vue/runtime-dom指向的就是runtime-dom包,应用esm版本就间接找xxx.esm-bundler.js文件,应用cjs版本就间接找xxx.cjs.js文件,前面不会再提到这个问题。

关上runtime-dom.esm-bundler.js文件,能够看到createApp办法:

import {  } from '@vue/runtime-core';export * from '@vue/runtime-core';import {  } from '@vue/shared';// ... 省略n多代码function createApp(...args) {    // ...}export {createApp};

能够看到runtime-dom包中还援用了runtime-core包和shared包,当初找到入口文件了,在剖析间接能够先搭建一个简略的代码剖析和测试的环境,这样不便本人验证并且能够间接看到代码的执行后果。

demo环境能够间接在本地搭建,也能够应用codesandboxstackblitz等在线环境,这里应用codesandbox,后续demo的代码都会放在codesandbox上,文末会有链接。

当然大家也能够间接在本地搭建一个demo环境,这里就不再赘述了。

源码剖析

下面的环境都筹备好了之后就能够间接开始剖析Vue3的源码了,咱们先来看一下createApp办法的实现;

createApp

const createApp = (...args) => {    const app = ensureRenderer().createApp(...args);    const {mount} = app;    app.mount = (containerOrSelector) => {        // ...    };    return app;}

createApp办法接管一个组件作为参数,而后调用ensureRenderer办法;

这个办法的作用是确保渲染器存在,如果不存在就创立一个渲染器,而后调用渲染器的createApp办法,这个办法的作用是创立一个利用实例,而后将这个利用实例返回,相当于一个单例模式。

let renderer;const ensureRenderer = () => renderer || (renderer = createRenderer(rendererOptions));

这里的rendererOptions是一些渲染器的配置,次要的作用是用来操作DOM的,这里不做过多的介绍,前面会有专门的文章来介绍。

当初先简略的来认识一下rendererOptions,这个外面会有两个办法前面会用到:

const rendererOptions = {    insert: (child, parent, anchor) => {        parent.insertBefore(child, anchor || null);    },    createText: text => document.createTextNode(text),}

当初咱们先简略的入手实现一下createApp办法,新建一个runtime-dom.js文件,而后内容如下:

import { createRenderer } from "./runtime-core";const createApp = (...args) => {  const rendererOptions = {    insert: (child, parent, anchor) => {      parent.insertBefore(child, anchor || null);    },    createText: (text) => document.createTextNode(text)  };  const app = createRenderer(rendererOptions).createApp(...args);  const { mount } = app;  app.mount = (containerOrSelector) => {    //...前面剖析再补上  };  return app;};export { createApp };

当初能够看到咱们在实现createApp办法的时候,间接调用了createRenderer办法,这个办法是创立渲染器的办法,这个办法的实现在runtime-core包中;

所以咱们须要补上runtime-core包中的createRenderer办法的实现;

createRenderer

createRenderer源码实现如下:

function createRenderer(options) {    return baseCreateRenderer(options);}// implementationfunction baseCreateRenderer(options, createHydrationFns) {    // 省略 n 多代码,都是函数定义,并会立刻执行,临时对后果不会有影响        return {        render,        hydrate,        createApp: createAppAPI(render, hydrate)    };}

createRenderer外部返回baseCreateRenderer办法的执行后果,这个办法的作用会返回renderhydratecreateApp三个办法;

而咱们最初须要调用的createApp办法就是在这三个办法中的其中一个,而createApp办法的是通过createAppAPI办法创立的,同时剩下的两个办法renderhydrate也是在createAppAPI办法中被调用的,所以咱们还须要看一下createAppAPI办法的实现;

createAppAPI

createAppAPI办法的实现如下:

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()    };}// 这个变量是用来统计创立的利用实例的个数let uid$1 = 0;function createAppAPI(render, hydrate) {    // 返回一个函数,这里次要是通过闭包来缓存下面传入的参数    return function createApp(rootComponent, rootProps = null) {        // rootComponent 就是咱们传入的根组件,这里会做一些校验                // 如果传递的不是一个函数,那么就做一个浅拷贝        if (!isFunction(rootComponent)) {            rootComponent = Object.assign({}, rootComponent);        }                // rootProps 就是咱们传入的根组件的 props,这个参数必须是一个对象        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();                // 通过 use 创立的插件都存在这里        const installedPlugins = new Set();                // 是否曾经挂载        let isMounted = false;                // 创立 app 对象        const app = (context.app = {            _uid: uid$1++,            _component: rootComponent,            _props: rootProps,            _container: null,            _context: context,            _instance: null,            version,            get config() {                // ...            },            set config(v) {                // ...            },            use(plugin, ...options) {                // ...            },            mixin(mixin) {                // ...            },            component(name, component) {                // ...            },            directive(name, directive) {                // ...            },            mount(rootContainer, isHydrate, isSVG) {                // ...            },            unmount() {                // ...            },            provide(key, value) {                // ...            }        });                // 返回 app 对象        return app;    };}

看到这里,咱们就能够晓得,createApp办法的实现其实就是在createAppAPI办法中返回一个函数,这个函数就是createApp办法;

这个办法并没有如许非凡,就是返回了一堆对象,这些对象就是咱们在应用createApp办法时,能够调用的办法;

这里能够看到咱们罕用的usemixincomponentdirectivemountunmountprovide等办法都是在app对象上的,也是通过这个函数制作并返回的;

当初咱们持续欠缺咱们的学习demo代码,当初新建一个runtime-core.js文件夹,而后把下面的代码复制进去;

然而咱们不能全都都间接照搬,下面的对象这么多的属性咱们只须要保留mount,因为还须要挂载能力看到成果,demo代码如下:

function createRenderer(options) {    // 先省略 render 和 hydrate 办法的实现,前面会讲到       return {        render,        hydrate,        createApp: createAppAPI(render, hydrate)    };}function createAppAPI(render, hydrate) {    return function createApp(rootComponent, rootProps = null) {        // 省略参数校验        rootComponent = Object.assign({}, rootComponent);                // 省略上下文的创立        const context = {            app: null        }                // 疏忽其余函数的实现,只保留 mount 函数和公有变量        let isMounted = false;        const app = (context.app = {            _uid: uid$1++,            _component: rootComponent,            _props: rootProps,            _container: null,            _context: context,            _instance: null,            mount(rootContainer, isHydrate, isSVG) {                // ...            },        });                return app;    };}

这样咱们就实现了createApp函数的简化版实现,接下来咱们就能够开始挂载了;

mount 挂载

下面咱们曾经学习到了createApp函数的实现,当初还须要通过mount办法来挂载咱们的根组件,能力验证咱们的demo代码是否正确;

咱们在调用createApp办法时,会返回一个app对象,这个对象上有一个mount办法,咱们须要通过这个办法来挂载咱们的根组件;

在这之前,咱们看到了createApp的实现中重写了mount办法,如下:

const createApp = (...args) => {    // ...省略其余代码        // 备份 mount 办法     const { mount } = app;        // 重写 mount 办法    app.mount = (containerOrSelector) => {        // 获取挂载的容器        const container = normalizeContainer(containerOrSelector);        if (!container)            return;                // _component 指向的是 createApp 传入的根组件        const component = app._component;                // 验证根组件是否是一个对象,并且有 render 和 template 两个属性之一        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.            // 确保模板是可信的,因为模板可能会有 JS 表达式,具体能够翻译下面的正文            component.template = container.innerHTML;        }                // clear content before mounting        // 挂载前清空容器        container.innerHTML = '';                // 正式挂载        const proxy = mount(container, false, container instanceof SVGElement);                // 挂载实现        if (container instanceof Element) {            // 革除容器的 v-cloak 属性,这也就是咱们常常看到的 v-cloak 的作用            container.removeAttribute('v-cloak');                        // 设置容器的 data-v-app 属性            container.setAttribute('data-v-app', '');        }                // 返回根组件的实例        return proxy;    };    return app;}

下面重写的mount办法中,其实最次要的做的是三件事:

  1. 获取挂载的容器
  2. 调用本来的mount办法挂载根组件
  3. 为容器设置vue的专属属性

当初到咱们入手实现一个简易版的mount办法了;

// 备份 mount 办法 const { mount } = app;// 重写 mount 办法app.mount = (containerOrSelector) => {    // 获取挂载的容器    const container = document.querySelector(containerOrSelector);    if (!container)        return;        const component = app._component;    container.innerHTML = '';        // 正式挂载    return mount(container, false, container instanceof SVGElement);};

这里的挂载其实还是应用的是createApp函数中的mount办法,咱们能够看到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)``);    }}

通过下面的一通剖析,其实挂载次要就是用的两个函数将内容渲染到容器中;

  1. createVNode 创立虚构节点
  2. render 渲染虚构节点

咱们这里就实现一个简易版的mount函数,来模仿挂载过程,代码如下:

function mount(rootContainer, isHydrate) {    // createApp 中传递的参数在咱们这里必定是一个对象,所以这里不做创立虚构节点的操作,而是模仿一个虚构节点    const vnode = {        type: rootComponent,        children: [],        component: null,    }    // 通过 render 函数渲染虚构节点    render(vnode, rootContainer);        // 返回 app 实例    return vnode.component}

虚构节点

虚构节点在Vue中曾经是十分常见的概念了,其实就是一个js对象,蕴含了dom的一些属性,比方tagpropschildren等等;

Vue3中保护了一套本人的虚构节点,大略信息如下:

export interface VNode {    __v_isVNode: true;    __v_skip: true;    type: VNodeTypes;    props: VNodeProps | null;    key: Key | null;    ref: Ref<null> | null;    scopeId: string | null;    children: VNodeNormalizedChildren;    component: ComponentInternalInstance | null;    suspense: SuspenseBoundary | null;    dirs: DirectiveBinding[] | null;    transition: TransitionHooks<null> | null;    el: RendererElement | null;    anchor: RendererNode | null;    target: RendererNode | null;    targetAnchor: RendererNode | null;    staticCount: number;    shapeFlag: ShapeFlags;    patchFlag: number;    dynamicProps: string[] | null;    dynamicChildren: VNode[] | null;    appContext: AppContext | null;}

残缺的type信息太多,这里就只贴VNode的相干定义,而且这些在Vue的实现中也没有那么简略,这一章不做具体的剖析,只是做一个简略的概念介绍;

render

render函数是在讲createRenderer的时候呈现的,是在baseCreateRenderer中定义的,具体源码如下:

function baseCreateRenderer(options, createHydrationFns) {    // ...        // 创立 render 函数    const render = (vnode, container, isSVG) => {        // 如果 vnode 不存在,并且容器是产生过渲染,那么将执行卸载操作        if (vnode == null) {            // container._vnode 指向的是上一次渲染的 vnode,在这个函数的最初一行            if (container._vnode) {                unmount(container._vnode, null, null, true);            }        }                // 执行 patch 操作,这里不做具体的剖析,牵扯太大,前面会独自讲        else {            patch(container._vnode || null, vnode, container, null, null, null, isSVG);        }                // 刷新工作队列,通常指代的是各种回调函数,比方生命周期函数、watcher、nextTick 等等        // 这里不做具体的剖析,前面会独自讲        flushPreFlushCbs();        flushPostFlushCbs();                // 记录 vnode,当初的 vnode 曾经是上一次渲染的 vnode 了        container._vnode = vnode;    };        // ...        return {        render,        hydrate,        createApp: createAppAPI(render, hydrate)    };}

render函数的次要作用就是将虚构节点渲染到容器中,unmount函数用来卸载容器中的内容,patch函数用来更新容器中的内容;

当初来实现一个简易版的render函数:

const render = (vnode, container) => {        patch(container._vnode || null, vnode, container);        // 记录 vnode,当初的 vnode 曾经是上一次渲染的 vnode 了    container._vnode = vnode;}

unmount函数不是咱们这次次要学习的内容,所以这里不做具体的剖析;

patch函数是Vue中最外围的函数,这次也不做具体的剖析,前面会独自讲,然而要验证咱们这次的学习成绩,所以咱们须要一个只有挂载性能的patch函数,这里咱们就本人实现一个简略的patch函数;

patch

patch函数的次要作用就是将虚构节点渲染到容器中,patch函数也是在baseCreateRenderer中定义的;

patch函数这次就不看了,因为外部的实现会牵扯到十分多的内容,这次只是它的呈现只是走个过场,前面会独自讲;

咱们这次的目标只是验证咱们这次源码学习的成成绩,所以咱们只须要一个只有挂载性能的patch函数,这里咱们就本人实现一个简略的patch函数;

// options 是在创立渲染器的时候传入的,还记得在 createApp 的实现中,咱们传入了一个有 insert 和 createText 办法的对象吗?不记得能够往上翻翻const { insert: hostInsert, createText: hostCreateText} = options;// Note: functions inside this closure should use `const xxx = () => {}`// style in order to prevent being inlined by minifiers./** * 简易版的实现,只是删除了一些不必要的逻辑 * @param n1 上一次渲染的 vnode * @param n2 以后须要渲染的 vnode * @param container 容器 * @param anchor 锚点, 用来标记插入的地位 */const patch = (n1, n2, container, anchor = null) => {    // 上一次渲染的 vnode 和以后须要渲染的 vnode 是同一个 vnode,那么就不须要做任何操作    if (n1 === n2) {        return;    }        // 获取以后须要渲染的 vnode 的类型    const { type } = n2;    switch (type) {        // 如果是文本节点,那么就间接创立文本节点,而后插入到容器中        case Text:            processText(n1, n2, container, anchor);            break;                    // 还会有其余的类型,这里不做具体的剖析,前面会独自讲                    // 其余的状况也会有很多种状况,这里对立当做是组件解决        default:            processComponent(n1, n2, container, anchor);    }};

patch函数的次要作用就是将虚构节点正确的渲染到容器中,这里咱们只实现了文本节点和组件的渲染,其余的类型的节点,前面会独自讲;

而咱们在应用createApp的时候,通常会传入一个根组件,这个根组件就会走到processComponent函数中;

所以咱们这里还须要实现了一个简略的processComponent函数;

const processComponent = (n1, n2, container, anchor) => {   if (n1 == null) {       mountComponent(n2, container, anchor);   }   // else {   //     updateComponent(n1, n2, optimized);   // }};

processComponent函数也是定义在baseCreateRenderer中的,这里还是和patch函数一样,只是实现了一个简略的性能,前面会独自讲;

processComponent函数做了两件事,一个是挂载组件,一个是更新组件,这里咱们只实现了挂载组件的性能;

挂载组件是通过mountComponent函数实现的,这个函数也是定义在baseCreateRenderer中的,然而咱们这次就不再持续深刻外部调用了,间接实现一个繁难的:

const mountComponent = (initialVNode, container, anchor) => {    // 通过调用组件的 render 办法,获取组件的 vnode    const subTree = initialVNode.type.render.call(null);        // 将组件的 vnode 渲染到容器中,间接调用 patch 函数    patch(null, subTree, container, anchor);};

这样咱们就实现了一个简易版的挂载组件的性能,这里咱们只是简略的调用了组件的render办法,render办法会返回一个vnode,而后调用patch函数将vnode渲染到容器中;

当初回头看看patch函数,还差一个processText函数没有实现,这个函数也是定义在baseCreateRenderer中的,这个比较简单,上面的代码就是实现的processText函数:

const processText = (n1, n2, container, anchor) => {    if (n1 == null) {        hostInsert((n2.el = hostCreateText(n2.children)), container, anchor);    }    // else {    //     const el = (n2.el = n1.el);    //     if (n2.children !== n1.children) {    //         hostSetText(el, n2.children);    //     }    // }};

我这里屏蔽掉了更新的操作,这里只管挂载,这里的hostInserthostCreateText函数就是在咱们实现繁难patch函数的时候,在patch函数实现的下面,通过解构赋值获取的,没印象能够回去看看;

验证

当初咱们曾经实现了一个简易版的createApp函数,并且咱们能够通过createApp函数创立一个利用,而后通过mount办法将利用挂载到容器中;

咱们能够通过上面的代码来验证一下:

import { createApp } from "./runtime-dom";const app = createApp({  render() {    return {      type: "Text",      children: "hello world"    };  }});app.mount("#app");

源码在codesandbox下面,能够间接查看:https://codesandbox.io/s/gallant-sun-khjot0?file=/src/main.js

总结

咱们通过浏览Vue3的源码,理解了Vue3createApp函数的实现,createApp函数是Vue3的入口函数,通过createApp函数咱们能够创立一个利用;

createApp的实现是借助了createRenderer函数,createRenderer的实现就是包装了baseCreateRenderer

baseCreateRenderer函数是一个工厂函数,通过baseCreateRenderer函数咱们能够创立一个渲染器;

baseCreateRenderer函数接管一个options对象,这个options对象中蕴含了一些渲染器的配置,比方insertcreateText等;

这些配置是在runtime-dom中实现的,runtime-dom中的createApp函数会将这些配置透传递给baseCreateRenderer函数,而后baseCreateRenderer函数会返回一个渲染器,这个渲染器中有一个函数就是createApp

createApp函数接管一个组件,而后返回一个利用,这个利用中有一个mount办法,这个mount办法就是用来将利用挂载到容器中的;

createApp中重写了mount办法,外部的实现是通过调用渲染器的mount办法;

这个mount办法是在baseCreateRenderer函数中实现的,baseCreateRenderer函数中的mount办法会调用patch函数;

patch函数外部会做很多的事件,尽管咱们这里只实现了挂载的逻辑,然而也是粗窥了patch函数的外部一些逻辑;

最初咱们实现了一个精简版的createApp函数,通过这个函数咱们能够创立一个利用,而后通过mount办法将利用挂载到容器中,这个过程中咱们也理解了Vue3的一些实现细节;

这次就到这里,下次咱们会持续深刻理解Vue3的源码,心愿大家可能多多反对,谢谢大家!