组件是我们用 Vue 做开发时经常用的功能模块,虽然多数时候我们是写的单文件组件,有时候也说到局部组件和全局组件,其实它们的底层都是用的 Vue.component 函数进行的组件注册,这一节我们来详细研究学习 Vue 的组件注册。
如何使用
我们先简单看下注册组件的例子:
// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))
// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })
// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')
过程分析
在前面 Vue 类的定义 章节我们研究了 Vue 的定义流程,其中有个 initGlobalAPI 函数,它里面又调用了 initAssetRegisters 函数定义 Vue.component 静态方法,我们看代码:
file: /src/core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else { // 定义 Vue.component 注册组件 的静态方法
if (type === 'component' && isPlainObject(definition)) {
// 确保有 name 属性
definition.name = definition.name || id
// this.options._base === Vue
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
如注释所述,对于注册组件的流程,先确定用户传入的 definition.name 属性,没有值则赋值 id,然后调用 this.options._base.extend,因为是静态方法,所以它等价于调用 Vue.extend(definition),并把该返回值放入 Vue.options[‘components’] 对象中。
Vue.extend
下面我们来看看 Vue.extend,它的作用是使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。它在函数 initExtend 中定义:
file: /src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {
Vue.cid = 0
let cid = 1
// Class inheritance
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
// 从缓存中获取组件构造函数(如果存在的话)
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
// 定义子类构造函数,同 Vue
const Sub = function VueComponent (options) {
this._init(options)
}
// 原型继承自 Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并 options 选项
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 初始化处理 props 属性,代理到 vm 上
if (Sub.options.props) {
initProps(Sub)
}
// 初始化处理 computed 计算属性,响应式化并代理到 vm 上
if (Sub.options.computed) {
initComputed(Sub)
}
// 启用 extend\mixin\use 等方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// Vue.component\Vue.directive\Vue.filter 赋给子类
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// 构造类放入自身 options.components 中
if (name) { Sub.options.components[name] = Sub }
// 缓存多种 options,后续实例化类时需用以检查更新
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 缓存子类构造函数,在局部注册组件时防止重复创建构造函数
cachedCtors[SuperId] = Sub
return Sub
}
}
如以上代码注释所述,Vue.extend 函数根据传入的扩展选项创建一个 Sub 构造函数,原型继承于 Vue,然后合并扩展选项和 Vue.options 为 Sub.options,再处理 props 和 computed 属性,扩展静态方法等。
最后把 Vue.options、扩展选项等引用保存在 Sub 静态属性上,在后续实例化组件时用来检查更新选项的操作。在函数的最后返回 Sub 构造函数,在 Vue.component 中把它存入 Vue.options.components 对象中以完成全局注册。
局部注册
局部注册我们在模块化开发中经常用到,比如在 Babel 和 webpack 使用 ES2015 模块来定义组件,然后像下面这样注册:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
或者直接
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components 对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。局部注册的组件的构造函数是在创建其对应的 Vnode 对象时在函数 createComponent 中才创建的。
在 createComponent 中也是通过调用 Vue.extend 创建的局部组件的构造函数,如果创建组件的 Vnode 多次的话,那么构造函数也会重复创建吗?答案是否定的,在 Vue.extend 中创建子类之后会把子类构造函数存入 Vue.extend 的实参 extendOptions._Ctor 中,即是 cachedCtors 中,它会在第二次 extend 时直接返回(见上面 Vue.extend 源码及注释)。
总结:
不管是全局注册的 Vue.component 方法还是局部注册的 options.components 选项属性,它们底层都是调用的 Vue.extend 去继承父类(通常是 Vue)创建子类构造函数,只是 extend 的时机不同。全局注册的组件会在 Vue.options.components 中存储,局部的会在 Vue 的对象实例 vm.$options.components 中存储。