关于javascript:源码库细数-Vue3-的实例方法和属性背后的故事

1次阅读

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

上一章咱们翻看了 Vue3 的源码的 createApp 的实现,并实现了一个简略的createApp

在翻看的过程中咱们发现了 Vue3createApp的实现中,有很多的办法,比方 mountprovideusecomponentdirectivemixin 等等;

而这些办法在官网文档中都是有提及的,明天咱们就来扒一扒 Vue3 API 参考 -> 利用实例背地的实现。

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

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

系列章节:

  • 【源码 & 库】跟着 Vue3 学习前端模块化
  • 【源码 & 库】在调用 createApp 时,Vue 为咱们做了那些工作?

首发在掘金,所以链接都是掘金的,无任何引流的意思。

初窥

咱们先来看看官网上的 Vue3API 利用实例 的列表:

上一章咱们曾经实现了 createApp 的办法,同时也晓得了 createApp 的办法返回的是一个 app 对象;

列表中从 mount 开始的办法都是 app 对象的办法,而 mount 在上一章中咱们也曾经实现了,所以咱们明天就看前面的办法。

同时列表的前面还有一些实例属性,这些也是在咱们明天的学习工作中的;

接下来的学习就是边跟着官网文档的介绍,边看源码的实现,来相熟这些办法和属性;

在上一节中咱们理解到了,createApp返回的 app 对象是在 createAppAPI 中创立的,咱们先来回顾一下 createAppAPI 的实现:

let uid$1 = 0;
function createAppAPI(render, hydrate) {return function createApp(rootComponent, rootProps = null) {
        // ...
        
        const context = createAppContext();
        const installedPlugins = new Set();
        let isMounted = false;
        const app = (context.app = {
            _uid: uid$1++,
            _component: rootComponent,
            _props: rootProps,
            _container: null,
            _context: context,
            _instance: null,
            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) {
                // ...
                return app;
            },
            mixin(mixin) {
                // ...
                return app;
            },
            component(name, component) {
                // ...
                return app;
            },
            directive(name, directive) {
                // ...
                return app;
            },
            mount(rootContainer, isHydrate, isSVG) {// ...},
            unmount() {// ...},
            provide(key, value) {
                // ...
                return app;
            }
        });
        return app;
    };
}

通过下面的代码能够看到的,官网介绍的 app 实例办法是一个也不少都在这;

至于属性的话,versionconfig 也是都在,不同的是 config 是一个 gettersetter,是通过上下文中的 config 来实现的;

上下文的 context 是在 createAppContext 中创立的,这个在上一章中也有提及,遗记了的能够回顾一下;

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()};
}

官网的介绍的实例属性在这里也是一个都不少都能够找到,不过这里还多了很多其余属性,置信通过咱们的学习,这些属性的作用也会一一理解到;

上面就开始正式的学习吧。

实例办法

unmount

unmount办法的作用是卸载利用实例,官网文档的介绍如下:

卸载一个已挂载的利用实例。卸载一个利用会触发该利用组件树内所有组件的卸载生命周期钩子。

函数签名如下:

interface App {unmount(): void
}

看一下源码的实现:

function unmount() {
    // 确认利用曾经挂载
    if (isMounted) {
        // 应用 render 办法卸载利用
        render(null, app._container);
        
        // 在非生产环境下,会革除利用组件的实例
        // 并且会调用 devtoolsUnmountApp 办法, 用于卸载 devtools 插件中的利用
        if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
            app._instance = null;
            devtoolsUnmountApp(app);
        }
        
        // 删除 dom 节点上缓存的 vue 实例
        delete app._container.__vue_app__;
    }
    
    // 在非生产环境下,如果利用没有挂载,会提醒该利用没有挂载
    else if ((process.env.NODE_ENV !== 'production')) {warn(`Cannot unmount an app that is not mounted.`);
    }
}

能够看到,unmount办法的实现也是比较简单的;

卸载利用实例就是卸载利用的根组件,卸载根组件就是调用 render 办法,将根组件渲染成null

render在上一章中简略的意识了一下,这一章并不筹备深刻的学习render,就简略的介绍一下;

render实质就是调用了 patch 办法,patch办法就是 Vue 的外围,它的作用是将 VNode 渲染成实在的DOM

render办法的第一个参数就是新的 VNode,如果是null 就会卸载掉旧的VNode,从而实现卸载组件的成果;

如果等不及想要理解 patch 办法的实现,能够在网上查查材料,网上有很多对于 patch 办法的解析,文章和视频都十分多,能够自行抉择;

provide

provide办法的作用是提供一个能够被注入的值,官网文档的介绍如下:

提供一个值,能够在利用中的所有后辈组件中注入应用。

函数签名如下:

interface InjectionKey<T> extends Symbol {}

interface App {
    /**
     * @param key 注入的值的 key
     * @param value 注入的值
     */
    provide<T>(key: InjectionKey<T> | symbol | string, value: T): this
}

从函数签名能够看出,provide办法接管两个参数,第一个参数是key,第二个参数是value

key能够是 InjectionKeysymbol 或者 stringvalue 能够是任意类型的值;

provide办法的实现如下:

function provide(key, value) {
    // 非生产环境下,如果 key 曾经存在于 context.provides 中,会提醒笼罩
    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.`);
    }
    
    // 将 key 和 value 存储到上下文的 provides 中
    context.provides[key] = value;
    
    // 返回 app 实例,不便链式调用
    return app;
}

能够看到,provide办法的实现也是非常简单的,间接将 keyvalue存储到 contextprovides属性中即可;

component

component办法的作用是注册一个全局组件,官网文档的介绍如下:

如果同时传递一个组件名字符串及其定义,则注册一个全局组件;如果只传递一个名字,则会返回用该名字注册的组件 (如果存在的话)。

函数签名如下:

interface App {component(name: string): Component | undefined
  component(name: string, component: Component): this
}

从函数签名能够看出,component办法有一个重载,一个是接管一个参数,一个是接管两个参数;

如果只传递一个参数,那么就是获取一个全局组件,如果传递两个参数,那么就是注册一个全局组件;

component办法的实现如下:

function component(name, component) {
    // 非生产环境下,会校验组件名是否非法
    if ((process.env.NODE_ENV !== 'production')) {validateComponentName(name, context.config);
    }
    
    // 如果只传递一个参数,那么就是获取一个全局组件
    if (!component) {return context.components[name];
    }
    
    // 非生产环境下,如果组件曾经存在,会提醒曾经注册,这里没有 return,所以会被笼罩
    if ((process.env.NODE_ENV !== 'production') && context.components[name]) {warn(`Component "${name}" has already been registered in target app.`);
    }
    
    // 将组件存储到上下文的 components 中
    context.components[name] = component;
    
    // 返回 app 实例,不便链式调用
    return app;
}

provide 办法相似,component办法的实现也就是将组件存储到 contextcomponents属性中,这里只是多了一个函数重载的性能;

directive

directive办法的作用是注册一个全局指令,官网文档的介绍如下:

如果同时传递一个名字和一个指令定义,则注册一个全局指令;如果只传递一个名字,则会返回用该名字注册的指令 (如果存在的话)。

函数签名如下:

interface App {directive(name: string): Directive | undefined
  directive(name: string, directive: Directive): this
}

component 办法雷同,directive办法也有一个重载,一个参数是获取全局指令,两个参数是注册全局指令;

directive办法的实现如下:

function directive(name, directive) {
    // 非生产环境下,会校验指令名是否非法
    if ((process.env.NODE_ENV !== 'production')) {validateDirectiveName(name);
    }
    
    // 如果只传递一个参数,那么就是获取一个全局指令
    if (!directive) {return context.directives[name];
    }
    
    // 非生产环境下,如果指令曾经存在,会提醒曾经注册,这里没有 return,所以会被笼罩
    if ((process.env.NODE_ENV !== 'production') && context.directives[name]) {warn(`Directive "${name}" has already been registered in target app.`);
    }
    
    // 将指令存储到上下文的 directives 中
    context.directives[name] = directive;
    
    // 返回 app 实例,不便链式调用
    return app;
}

和之前的都一样,directive办法的实现也是将指令存储到 contextdirectives属性中;

use

use办法的作用是注册一个插件,官网文档的介绍如下:

装置一个插件。

非常简单的一句话,还是先看看函数签名:

type PluginInstallFunction<Options> = Options extends unknown[]
    ? (app: App, ...options: Options) => any
    : (app: App, options: Options) => any

type Plugin<Options = any[]> =
    | (PluginInstallFunction<Options> & {install?: PluginInstallFunction<Options>})
    | {install: PluginInstallFunction<Options>}

interface App {use(plugin: Plugin, ...options: any[]): this
}

use办法其实只有一个参数,就是插件,插件的类型是 Plugin,能够看到Plugin 的类型定义,看起来略微有点简单,来合成一下:

type PluginInstallFunction<Options> = Options extends unknown[]
    ? (app: App, ...options: Options) => any
    : (app: App, options: Options) => any

这里的 Options 是一个泛型,能够传递任意类型,当然这个并不是重点,重点是 unknown 类型;

unknown类型是 ts3.0 新增的类型,它是任意类型的子类型,然而没有任何类型是unknown 的子类型,也就是说 unknown 类型不能赋值给其余类型,除非是 any 类型;

下面说的太官网了,其实间接将 unknown 类型了解为 any 类型就能够了,不同于 any 类型能够间接应用,unknown类型必须先进行类型断言,能力应用:

const value: unknown = 123;

const num: number = value; // 报错,不能将 unknown 类型赋值给 number 类型
console.log(value + 1); // 报错,不能将 unknown 类型的值进行数学运算

if (typeof value === 'number') {
    const num: number = value; // 正确,先进行类型断言,能力应用
    console.log(value + 1); // 正确,先进行类型断言,能力应用
}

// 还有其余的状况,这里只是举个例子

能够看到这里说的类型断言就是应用 js 中的 typeof 来判断类型,unknown类型的值就能够失常应用了;

其实在 PluginInstallFunction 这个类型定义中,extends unknown[]也是一个类型断言,这里的断言就是判断 Options 是否是一个数组,如果是数组,转换成 js 的写法如下:

function PluginInstallFunction(Options) {if (Array.isArray(Options)) {return function (app, ...options) {// ...}
    } else {return function (app, options) {// ...}
    }
}

这里弄清楚了之后就可以看 Plugin 的类型定义了:

type Plugin<Options = any[]> =
    | (PluginInstallFunction<Options> & {install?: PluginInstallFunction<Options>})
    | {install: PluginInstallFunction<Options>}

Plugin是一个联结类型,外面就应用了 PluginInstallFunction 这个类型;

能够看到的是 Plugin 的泛型是有默认值的,这个默认值就是any[],并且这个泛型的值会传递给PluginInstallFunction

所以默认状况下,Plugin的类型是这样的:

type Plugin = 
    | (PluginInstallFunction<any[]>
        & {install?: PluginInstallFunction<any[]> }
      )
    | {install: PluginInstallFunction<any[]> 
      }

这样看就很分明了,Plugin的类型就是一个函数或者一个对象:

  • 函数状况下,能够有 install 属性,也能够没有 install 属性;
  • 对象状况下,必须有 install 属性;

Plugin的类型定义就是这样,接下来看看 use 办法的实现:

// 寄存曾经注册过的插件
const installedPlugins = new Set();

function use(plugin, ...options) {
    // 判断插件是否曾经注册过
    if (installedPlugins.has(plugin)) {
        // 非生产环境下,会提醒曾经注册过
        (process.env.NODE_ENV !== 'production') && warn(`Plugin has already been applied to target app.`);
    }
    
    // 判断插件是有 install 属性
    else if (plugin && isFunction(plugin.install)) {
        // 将插件增加到 installedPlugins 中
        installedPlugins.add(plugin);
        // 调用插件的 install 办法
        plugin.install(app, ...options);
    }
    
    // 如果插件是一个函数,就间接调用
    else if (isFunction(plugin)) {
        // 将插件增加到 installedPlugins 中
        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.`);
    }
    
    // 返回 app 实例,不便链式调用
    return app;
}

use办法的实现相对来说会代码多一些,其实次要是在判断插件的类型;

这里的插件其实就是给开发者提供的一个扩大点,开发者能够在这里注册本人的插件,而后在插件中能够扩大 vue 的性能;

应用 use 办法注册插件的形式有两种:

// 形式一
const plugin = (app, options) => {// ...};
app.use(plugin, options);

// 形式二
const plugin = {install(app, options) {// ...}
};
app.use(plugin);

// 同形式二
function plugin(app, options) {// ...}
plugin.install = (app, options) => {// ...}
app.use(plugin);

use办法就就到这里了,毕竟是源码解析,题外话不讲太多。

mixin

mixin办法是用来注册一个混入对象,这个混入对象会在作用与整个组件实例,官网的解释如下:

利用一个全局 mixin (实用于该利用的范畴)。一个全局的 mixin 会作用于利用中的每个组件实例。

留神:官网曾经明确批示不再举荐应用mixin

来看一下 mixin 的函数定义:

interface App {mixin(mixin: ComponentOptions): this
}

mixin办法的参数是一个 ComponentOptions 类型,这个其实就是一个组件的配置对象,这个配置对象会在作用与整个组件实例;

mixin办法的实现如下:

function mixin(mixin) {
    // 判断是否反对 Options API
    if (__VUE_OPTIONS_API__) {
        // 判断是否曾经注册过
        if (!context.mixins.includes(mixin)) {
            // 将 mixin 增加到 context.mixins 中
            context.mixins.push(mixin);
        }
        
        // 曾经注册过,提醒错误信息
        else if ((process.env.NODE_ENV !== 'production')) {
            warn('Mixin has already been applied to target app' +
                (mixin.name ? `: ${mixin.name}` : ''));
        }
    }
    
    // 不反对 Options API 是不能应用 mixin 的
    else if ((process.env.NODE_ENV !== 'production')) {warn('Mixins are only available in builds supporting Options API');
    }
    
    // 返回 app 实例,不便链式调用
    return app;
}

mixin办法的实现也很简略,就是将 mixin 增加到 context.mixins 中,这里的 context.mixins 是一个数组,所以能够注册多个mixin

值得注意的是,mixin办法只能在反对 Options API 的环境下应用,不反对 Options API 的环境下会提醒错误信息;

开启 Options API 的形式也很简略,尽管是默认开启的,然而能够有配置开关:

截图文档地址:https://github.com/vuejs/core/tree/main/packages/vue

实例属性

version

version属性是用来获取以后 vue 的版本号,官网的解释如下:

提供以后利用所应用的 Vue 版本号。这在插件中很有用,因为可能须要依据不同的 Vue 版本执行不同的逻辑。

这个其实没啥好说的,就是一个字符串,值就是以后 vue 的版本号,官网的介绍也说了在插件中很有用,这里就不多说了。

这里的源码也没必要看,是通过打包工具生成的,感兴趣能够看我的第一篇文章;

config

config属性是用来获取以后 vue 的配置,官网的解释如下:

每个利用实例都会裸露一个 config 对象,其中蕴含了对这个利用的配置设定。你能够在挂载利用前更改这些属性 (上面列举了每个属性的对应文档)。

config 属性的实现是通过 getset办法实现的:

const app = {
    // ...
    get config() {
        // 返回上下文的 config 属性
        return context.config;
    },
    set config(v) {
        // config 属性是只读的,不能被批改
        if ((process.env.NODE_ENV !== 'production')) {warn(`app.config cannot be replaced. Modify individual options instead.`);
        }
    }
    // ...
};

MDN get

MDN set

能够看到的是 config 属性指向的是上下文的 config 属性,并且 config 属性是只读的,不能被批改;`

config.errorHandler

config.errorHandler属性是用来获取以后 vue 的谬误处理函数,官网的解释如下:

用于为利用内抛出的未捕捉谬误指定一个全局处理函数。

看一下 errorHandler 的定义:

interface AppConfig {
    errorHandler?: (
        err: unknown,
        instance: ComponentPublicInstance | null,
        // `info` 是一个 Vue 特定的错误信息
        // 例如:谬误是在哪个生命周期的钩子上抛出的
        info: string
    ) => void
}

通过签名能够看出,errorHandler是一个函数,它有三个参数:

  • err:谬误对象
  • instance:组件实例
  • info:谬误起源类型信息

留神:属性都没有实现,只是定义了类型,有调用机会,前面会缓缓剖析。

config.warnHandler

config.warnHandler属性是用来获取以后 vue 的正告处理函数,官网的解释如下:

用于为 Vue 的运行时正告指定一个自定义处理函数。

看一下 warnHandler 的定义:

interface AppConfig {
  warnHandler?: (
    msg: string,
    instance: ComponentPublicInstance | null,
    trace: string
  ) => void
}

通过签名能够看出,warnHandler是一个函数,它有三个参数:

  • msg:正告信息
  • instance:组件实例
  • trace:谬误堆栈

errorHandler 很相似,然而最初一个参数不一样,这个参数是谬误堆栈,能够通过 console.trace 打印进去;

config.performance

config.performance属性是用来获取以后 vue 的性能监控开关,官网的解释如下:

设置此项为 true 能够在浏览器开发工具的“性能 / 工夫线”页中启用对组件初始化、编译、渲染和修补的性能体现追踪。仅在开发模式和反对 performance.mark API 的浏览器中工作。

是一个布尔值,次要是做性能优化的,用来剖析组件的初始化、编译、渲染和修补的性能体现,这个属性只在开发模式下无效;

config.compilerOptions

config.compilerOptions属性是用来开启运行时的编译器,官网的解释如下:

配置运行时编译器的选项。设置在此对象上的值将会在浏览器内进行模板编译时应用,并会影响到所配置利用的所有组件。另外你也能够通过 compilerOptions 选项在每个组件的根底上笼罩这些选项。

次要是针对模板编译的开启配置,例如有 template 属性的组件,会在浏览器内进行模板编译;

留神:开启这个属性会减少打包体积。

在 compilerOptions 配置属性中还有其余配置,因为本系列是源码剖析系列,所以这里就不一一介绍了,感兴趣的能够看官网文档。

config.globalProperties

config.globalProperties属性是用来注册全局属性,官网的解释如下:

一个用于注册可能被利用内所有组件实例拜访到的全局属性的对象。

看一下 globalProperties 的定义:

interface AppConfig {globalProperties: Record<string, any>}

通过签名能够看出,globalProperties是一个对象,它的属性会被注册到全局,能够被利用内所有组件实例拜访到;

这个属性次要应用用来代替 Vue2 中的Vue.prototype,例如:

// Vue2
Vue.prototype.$message = function() {// ...}

// Vue3
app.config.globalProperties.$message = function() {// ...}

config.optionMergeStrategies

config.optionMergeStrategies属性是用来自定义合并策略,官网的解释如下:

一个用于定义自定义组件选项的合并策略的对象。

看一下 optionMergeStrategies 的定义:

interface AppConfig {optionMergeStrategies: Record<string, OptionMergeFunction>}

type OptionMergeFunction = (to: unknown, from: unknown) => any

合并策略是用来合并组件的一些配置,例如 datacomputedmethods 等,这个属性次要是用来自定义合并策略,例如:

合并策略是一个十分乏味的货色,它能够让咱们自定义合并策略,例如:

const app = createApp({
    // option from self
    msg: 'Vue',
    // option from a mixin
    mixins: [
        {msg: 'Hello'}
    ],
    mounted() {
        // 在 this.$options 上裸露被合并的选项
        console.log(this.$options.msg)
    }
})

// 为  `msg` 定义一个合并策略函数
app.config.optionMergeStrategies.msg = (parent, child) => {return (parent || '') + (child ||'')
}

app.mount('#app')
// 打印 'Hello Vue'

下面的代码是官网的示例,这里只是简略的介绍一下,具体的合并策略能够看官网文档。

总结

通过这次的剖析,咱们理解到了 Vue3 通过 createApp 返回的实例办法和示例属性,以及理解到这些办法和属性的作用;

而这些办法和属性都是只注册,并没有真正的调用,这很容易激发咱们的好奇心,这些货色注册了,到底是什么时候调用的呢?

其实在 Vue3 中,还有很多 API 都是只注册,并没有真正的调用,前面咱们将接着来剖析这些只注册没有调用的API

跟着我的节奏来,当初不会进入外围源码,先从相熟 Vue3API开始,这样能力更好的了解外围源码,心愿大家不要焦急,慢慢来,一起学习,一起提高!

这次的剖析就到这里,如果感觉好的话,心愿大家能够点个赞反对一下,谢谢大家!

正文完
 0