Vue3 组件库的设计与实现
Vue3 全局组件注册,除了上面说到的使用 app.component() 进行注册以外,还可以使用插件进行注册。例如下面这种方式:
import { RouterView } from './router-view.js';
RouterView.install = app => app.component('RouterView', RouterView)
使用插件通过 app.component() 可以注册一到多个全局组件。那么 Vue3 的插件原理又是怎么样的呢?
插件原理
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:
import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
/* 可选的选项 */
})
我们可以看到插件是在 Vue3 应用实例对象上的一个方法,所以我们要去看 use 函数具体的实现:
function createAppAPI(render) {
return function createApp(rootComponent) {
// 插件注册池
const installedPlugins = new Set()
// 创建 Vue3 应用实例对象
const app = (context.app = {
// 注册插件方法
use(plugin, ...options) {
if (installedPlugins.has(plugin)) {
// 已经注册过的插件不运行再进行注册
console.warn(`Plugin has already been applied to target app.`)
} else if (plugin && typeof plugin.install === 'function') {
// 如果插件对象的 install 属性是一个函数,那么就通过调用插件对象的 install 方法进行插件注册
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (typeof plugin === 'function') {
// 如果插件对象本身是一个函数,那么就直接执行插件本身进行插件注册
installedPlugins.add(plugin)
plugin(app, ...options)
}
return app
},
// 省略相关代码
})
return app
}
}
在创建 Vue3 应用实例对象之前先创建一个插件安装池的变量用来保存已经安装过的插件,如果已经安装过的插件则不能再进行安装。一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数,执行安装函数之前会把插件往插件池变量中添加。
因为插件的安装函数可以接受到 Vue3 的应用实例对象,就可以通过 Vue3 的应用实例对象获取其他的方法,进行一些操作,比如:
-
通过
app.component()和app.directive()注册一到多个全局组件或自定义指令。 -
通过
app.provide()使一个资源可被注入进整个应用。 -
向
app.config.globalProperties中添加一些全局实例属性或方法。 -
一个可能上述三种都包含了的功能库 (例如
vue-router)。
组件库实现原理
接下来我们简单实现一个组件库。
首先我们编写两个组件。
组件库 Button 组件:
import { ref } from '../node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
import { createVNode } from './vnode.js'
export const Button = {
name: 'Button',
setup() {
const txt = ref('组件库 Button 组件')
return {
txt
}
},
render() {
return createVNode('div', {}, 'Button param txt is:' + this.txt)
}
}
组件库 Icon 组件 :
import { ref } from '../node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
import { createVNode } from './vnode.js'
export const Icon = {
name: 'Icon',
setup() {
const txt = ref('组件库 Icon 组件')
return {
txt
}
},
render() {
return createVNode('div', {}, 'Icon param txt is:' + this.txt)
}
}
组件库编写:
import { Button } from './Button.js';
import { Icon } from './Icon.js';
// 为每个组件安装插件
Button.install = app => app.component('Button', Button)
Icon.install = app => app.component('Icon', Icon)
// 组件库
const components = [
Button,
Icon
]
// 是否已安装标识
const INSTALLED_KEY = Symbol('INSTALLED_KEY')
// 组件库插件
export const ElementPlus = {
install(app) {
// 如果该组件库已经安装过了,则不进行安装
if (app[INSTALLED_KEY]) return;
// 将标识值设置为 true,表示已经安装了
app[INSTALLED_KEY] = true;
// 循环组件库中的每个组件进行安装
components.forEach((c) => app.use(c));
}
}
通过上面的代码我们可以知道 Vue3 组件库的基本实现原理:就是为每一个组件进行安装一个插件,然后通过插件进行组件的全局安装,再把所有的组件设置到一个数组中组成一个组件库,再编写一个组件库插件,在组件库插件里面进行循环数组组件库里的每一个组件,因为每一个组件都拥有插件所需的 install() 方法,所以每一个组件又是一个插件,又可以调用 use() 方法进行安装,最后就会执行每一个组件的 install() 方法,然后进行进行组件的全局安装,这样组件库里面的每个组件都将被注册到全局组件中去了。
这样设计的目的是为了自动化进行注册组件,不然一个组件库有几十个组件,不进行自动化注册的话,让用户一个个手动进行引入进行注册,则需要写大量重复的代码。现在设计成一个组件库插件,然后通过安装插件的方式进行注册,这样用户只需要引入一个组件库插件,然后安装一下就可以了,大大减少了代码量。
实战操作
在上面我提供的 Gitee 仓库:
中的文件夹 v3 中的代码则实现了完整的代码和详细的注释。
我们看一下调用的过程:
import { createApp } from './mini-vue3.js';
import { App } from './App.js';
import { ElementPlus } from './element-plus.js';
const app = createApp(App)
// 安装组件库
app.use(ElementPlus)
app.mount(document.querySelector("#app"));
App.js 中的调用过程:
import { ref } from '../node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
import { resolveComponent } from './resolveAssets.js'
import { createVNode } from './vnode.js'
export const App = {
name: 'App',
setup() {
const count = ref(520)
return {
count
}
},
render() {
// 获取组件库 Button 组件
const Button = resolveComponent('Button')
// 获取组件库 Icon 组件
const Icon = resolveComponent('Icon')
return createVNode('div', {}, [
createVNode('p', {}, 'Hi Vue3 Component param count is:' + this.count),
createVNode(Button),
createVNode(Icon)
])
}
}
接下来我们通过 VScode 插件 Live Server 运行 index.html 文件。
浏览器渲染结果如下:

至此我们手动编写的组件库也成功渲染到页面上了。
总结
此文章的篇幅虽然有点长,但我们始终围绕一个组件的运作机制为核心进行剖析。我们手写实现了一个 Vue3 组件是如何从诞生到渲染到页面上的整个过程,其实就是 Vue3 初始化的过程都做了什么。从中我们可以知道 Vue3 组件的本质是一个对象,最终是为了产生要渲染到页面的虚拟DOM,在渲染虚拟DOM 的过程中会根据虚拟DOM 的类型进行不通过的操作,主要是如果是组件类型那就进行组件的初始化,如果是元素类型那么就进行元素的创建。最后会把所有的虚拟DOM 都会渲染到页面上。
接着我们又手写实现了 Vue3 全局组件和局部组件的实现原理。全局组件是把组件注册到根组件上下文对象的 components 中,局部组件是把组件注册到所需组件的 components 选项中,在组件进行初始化的时候都会继承父组件的上下文对象,这样就可以在每一个组件中都能获取到全局组件了。SFC 组件中 template 中引用到的局部组件和全局组件最终都会编译成通过 resolveComponent 函数进行加载对应的组件,而在 resolveComponent 函数中则是通过当前的组件实例对象先进行获取局部组件,获取不到再进行获取全局组件。
在了解了 Vue3 全局组件和局部组件的实现原理之后,我们就可以了解组件库的实现原理了。组件库的基本实现原理就是为每一个组件安装一个插件所需的 install() 方法,在插件方法里面再进行把组件安装到全局组件中,然后把所有的组件设置到一个组件库中也就是一个数组,再添加一个组件库的插件,在组件库的插件中循环组件库,得每一个组件都是一个插件,再进行每一个组件的插件安装,最终把所有的组件都安装到全局组件中去。