【源码&库】细数 Vue3 的实例方法和属性背后的故事

初窥

我们先来看看官网上的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 | undefinedcomponent(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 | undefineddirective(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 APIif (__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的方式也很简单,虽然是默认开启的,但是可以有配置开关:

截图文档地址:github.com/vuejs/core/…

实例属性

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 selfmsg: 'Vue',// option from a mixinmixins: [{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' 

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

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/web2022050903/article/details/129406844