在调用 createApp 时,Vue 为我们做了那些工作?

17,165次阅读
没有评论

共计 14082 个字符,预计需要花费 36 分钟才能阅读完成。

寻找入口

先看一下 Vue3 的源码目录:

在调用 createApp 时,Vue 为我们做了那些工作?

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 文件,这个文件是 Vue3 的 ESM 版本,会通过 import 来引入其他模块,这样就可以直接看到 Vue3 的模块划分。
本系列就会通过 vue.esm-bundler.js 文件来分析 Vue3 的源码,并且会通过边分析边动手的方式来学习 Vue3 的源码。

在调用 createApp 时,Vue 为我们做了那些工作?

使用

我们先来看一下 Vue3 的使用方式:

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

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

在 Vue3 中,我们需要使用 createApp 来创建一个应用实例,然后使用 mount 方法将应用挂载到某个 DOM 节点上。
createApp 是从 vue 包中导出的一个方法,它接收一个组件作为参数,然后返回一个应用实例。

入口 createApp

从 vue 的 package.json 可以看到,module 字段指向了 dist/vue.esm-bundler.js 文件,这个文件是 Vue3 的 ESM 版本,我们可以直接使用 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 环境可以直接在本地搭建,也可以使用 codesandbox、stackblitz 等在线环境,这里使用 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);
}

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

createRenderer 内部返回 baseCreateRenderer 方法的执行结果,这个方法的作用会返回 render、hydrate、createApp 三个方法;
而我们最后需要调用的 createApp 方法就是在这三个方法中的其中一个,而 createApp 方法的是通过 createAppAPI 方法创建的,同时剩下的两个方法 render 和 hydrate 也是在 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 方法时,可以调用的方法;
这里可以看到我们常用的 use、mixin、component、directive、mount、unmount、provide 等方法都是在 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 的一些属性,比如 tag、props、children 等等;
在 Vue3 中维护了一套自己的虚拟节点,大概信息如下:

export interface VNode {
    __v_isVNode: true;
    __v_skip: true;
    type: VNodeTypes;
    props: VNodeProps | null;
    key: Key | null;
    ref: Ref | null;
    scopeId: string | null;
    children: VNodeNormalizedChildren;
    component: ComponentInternalInstance | null;
    suspense: SuspenseBoundary | null;
    dirs: DirectiveBinding[] | null;
    transition: TransitionHooks | 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);
    //     }
    // }
};

我这里屏蔽掉了更新的操作,这里只管挂载,这里的 hostInsert 和 hostCreateText 函数就是在我们实现简易 patch 函数的时候,在 patch 函数实现的上面,通过解构赋值获取的,没印象可以回去看看;

验证

现在我们已经实现了一个简易版的 createApp 函数,并且我们可以通过 createApp 函数创建一个应用,然后通过 mount 方法将应用挂载到容器中;
我们可以通过下面的代码来验证一下:

import {createApp} from "./runtime-dom";

const app = createApp({render() {
    return {
      type: "Text",
      children: "hello world"
    };
  }
});

app.mount("#app");

源码在 codesandbox 上面,可以直接查看:codesandbox.io/s/gallant-s…

总结

我们通过阅读 Vue3 的源码,了解了 Vue3 的 createApp 函数的实现,createApp 函数是 Vue3 的入口函数,通过 createApp 函数我们可以创建一个应用;
createApp 的实现是借助了 createRenderer 函数,createRenderer 的实现就是包装了 baseCreateRenderer;
baseCreateRenderer 函数是一个工厂函数,通过 baseCreateRenderer 函数我们可以创建一个渲染器;
baseCreateRenderer 函数接收一个 options 对象,这个 options 对象中包含了一些渲染器的配置,比如 insert、createText 等;
这些配置是在 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 的源码,希望大家能够多多支持,谢谢大家!

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-09-23发表,共计14082字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)