上一章我们翻看了Vue3
的源码的createApp
的实现,并实现了一个简单的createApp
;
在翻看的过程中我们发现了Vue3
的createApp
的实现中,有很多的方法,比如mount
、provide
、use
、component
、directive
、mixin
等等;
而这些方法在官方文档中都是有提及的,今天我们就来扒一扒Vue3 API 参考 -> 应用实例背后的实现。
大家好,这里是田八的【源码&库】系列,
Vue3
的源码阅读计划,Vue3
的源码阅读计划不出意外每周一更,欢迎大家关注。如果想一起交流的话,可以点击这里一起共同交流成长
系列章节:
首发在掘金,所以链接都是掘金的,无任何引流的意思。
初窥
我们先来看看官网上的Vue3
的API
的应用实例
的列表:
上一章我们已经实现了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
实例方法是一个也不少都在这;
至于属性的话,version
和config
也是都在,不同的是config
是一个getter
和setter
,是通过上下文中的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 extends Symbol {}
interface App {
/**
* @param key 注入的值的 key
* @param value 注入的值
*/
provide(key: InjectionKey | symbol | string, value: T): this
}
从函数签名可以看出,provide
方法接收两个参数,第一个参数是key
,第二个参数是value
;
key
可以是InjectionKey
、symbol
或者string
,value
可以是任意类型的值;
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
方法的实现也是非常简单的,直接将key
和value
存储到context
的provides
属性中即可;
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
方法的实现也就是将组件存储到context
的components
属性中,这里只是多了一个函数重载的功能;
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
方法的实现也是将指令存储到context
的directives
属性中;
use
use
方法的作用是注册一个插件,官方文档的介绍如下:
安装一个插件。
非常简单的一句话,还是先看看函数签名:
type PluginInstallFunction = Options extends unknown[]
? (app: App, ...options: Options) => any
: (app: App, options: Options) => any
type Plugin =
| (PluginInstallFunction & {
install?: PluginInstallFunction
})
| {
install: PluginInstallFunction
}
interface App {
use(plugin: Plugin, ...options: any[]): this
}
use
方法其实只有一个参数,就是插件,插件的类型是Plugin
,可以看到Plugin
的类型定义,看起来稍微有点复杂,来分解一下:
type PluginInstallFunction = Options extends unknown[]
? (app: App, ...options: Options) => any
: (app: App, options: Options) => any
这里的Options
是一个泛型,可以传递任意类型,当然这个并不是重点,重点是unknown
类型;
unknown
类型是ts
3.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 =
| (PluginInstallFunction & {
install?: PluginInstallFunction
})
| {
install: PluginInstallFunction
}
Plugin
是一个联合类型,里面就使用了PluginInstallFunction
这个类型;
可以看到的是Plugin
的泛型是有默认值的,这个默认值就是any[]
,并且这个泛型的值会传递给PluginInstallFunction
;
所以默认情况下,Plugin
的类型是这样的:
type Plugin =
| (
PluginInstallFunction
& { install?: PluginInstallFunction }
)
| {
install: PluginInstallFunction
}
这样看就很清楚了,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
属性的实现是通过get
和set
方法实现的:
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.`);
}
}
// ...
};
可以看到的是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
}
通过签名可以看出,globalProperties
是一个对象,它的属性会被注册到全局,可以被应用内所有组件实例访问到;
这个属性主要使用用来代替Vue2
中的Vue.prototype
,例如:
// Vue2
Vue.prototype.$message = function() {
// ...
}
// Vue3
app.config.globalProperties.$message = function() {
// ...
}
config.optionMergeStrategies
config.optionMergeStrategies
属性是用来自定义合并策略,官网的解释如下:
一个用于定义自定义组件选项的合并策略的对象。
看一下optionMergeStrategies
的定义:
interface AppConfig {
optionMergeStrategies: Record
}
type OptionMergeFunction = (to: unknown, from: unknown) => any
合并策略是用来合并组件的一些配置,例如data
、computed
、methods
等,这个属性主要是用来自定义合并策略,例如:
合并策略是一个非常有趣的东西,它可以让我们自定义合并策略,例如:
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
;
跟着我的节奏来,现在不会进入核心源码,先从熟悉Vue3
的API
开始,这样才能更好的理解核心源码,希望大家不要着急,慢慢来,一起学习,一起进步!
这次的分析就到这里,如果觉得好的话,希望大家可以点个赞支持一下,谢谢大家!