建议PC端观看,移动端代码高亮错乱
在 Vue.js
中,除了它内置的组件如 keep-alive
、component
、transition
、transition-group
等,其它用户自定义组件在使用前必须注册。很多同学在开发过程中可能会遇到如下报错信息:
一般报这个错的原因都是我们使用了未注册的组件。Vue
提供了全局和局部两种注册方式。
1. components策略
在这之前先了解在 mergeOptions
中的 components
策略,关于配置合并相关概念在之前的章节已经介绍过了。
// src/core/util/options.js
// components合并策略
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
// ...
return extend(res, childVal)
} else {
return res
}
}
// ASSET_TYPES: ['component', 'directive', 'filter']
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
可以看到 components
的合并策略很简单,返回一个 res
对象,其属性分别是 childVal
,其原型是 parentVal
。
简单回顾初始化时 mergeOptions
的逻辑:
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // Vue.options
options || {},
vm
)
}
// ...
}
对于根实例来说,当执行完 else
逻辑后:vm.$options.components.__proto__ === Vue.options.components
对于组件实例来说,当我们通过 Vue.extend
创建构造函数时:
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
// 启用递归自查
if (name) {
Sub.options.components[name] = Sub
}
// ...
return Sub
}
通过 mergeOptions
把 Vue.options
合并到 Sub.options
上。
Vue.options.components
长这样:
合并之后, Sub.options.components
长这样:
可以发现 Sub.options.components.__proto__ === Vue.options.components
回到我们的 Vue.extend
中去,还会执行这个逻辑:
// 启用递归自查
if (name) {
Sub.options.components[name] = Sub
}
通过指向自身,实现自查找,所以此时我们的 Sub.options.components
长这样:
然后在后续组件的实例化阶段,会执行 initInternalComponent
,其中的关键逻辑是:
vm.$options = Object.create(vm.constructor.options)
这样就可以通过 vm.$options.components
访问到 Sub.options.components
最后用一张图总结就是:
2. 全局注册
全局注册的例子:
Vue.component('my-component', {
// 选项
})
看下 Vue.component
函数的定义,代码在 src/core/global-api/assets.js
中:
// src/core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
// ASSET_TYPES: ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id) // 验证组件名
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
// ...
this.options[type + 's'][id] = definition
return definition
}
}
})
}
Vue.component
核心逻辑就是通过 Vue.extend
创建组件子类构造函数并挂载到 Vue.options.components[id]
至于为什么
this.options._base
是Vue
,在这里已经介绍过了
3. 局部注册
当传入 createElement
的参数是组件字符串时,需要使用 components
选项注册局部组件
const App = {
name: 'app',
render(h) {
return h('div', {}, 'hello vue')
},
}
new Vue({
el: '#app',
render(h) {
return h('App')
},
components: { App }
})
我们再来回顾一下 _createElement
的逻辑:
// src/core/vdom/create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
let vnode
if (typeof tag === 'string') {
let Ctor
// ...
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
// ...
}
- 当
tag
是一个字符串时:- 普通的
html
标签,直接创建vnode
- 已注册的组件字符串,解析得到组件构造函数,再通过
createComponent
创建组件的占位符vnode
- 创建一个未知的
vnode
- 普通的
- 当
tag
直接是一个组件对象时,通过createComponent
创建组件的占位符vnode
下面来看看 tag
是组件字符串的情况:
通过 resolveAsset
将 tag
字符串解析得到组件构造函数或组件对象(因为全局注册 Vue.component
函数中已经创建好了构造函数,而局部注册时需要额外的在 createComponent
中通过组件对象创建构造函数):
Ctor = resolveAsset(context.$options, 'components', tag)
resolveAsset
这个定义在 src/core/utils/options.js
中:
// src/core/utils/options.js
export function resolveAsset (
options: Object, // vm.$options
type: string, // components
id: string,
warnMissing?: boolean
): any {
if (typeof id !== 'string') {
return
}
const assets = options[type] // vm.$options.components
// 局部注册优先:在对象自身查找
if (hasOwn(assets, id)) return assets[id]
// camelize: app-child => appChild
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
// capitalize: appChild => AppChild
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// 去原型链找全局注册
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
其中 camelize
函数简化后:
const camelizeRE = /-(\w)/g
function camelize(str) {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}
其中 capitalize
函数简化后:
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
})
resolveAsset
的主要逻辑如下:
- 在
vm.$options.components
对象的自身分别查找局部注册的组件构造函数vm.$options.components[id]
vm.$options.components[camelizedId]
vm.$options.components[PascalCaseId]
- 在
vm.$options.components
的原型上分别查找全局注册的组件构造函数vm.$options.components.__proto__[id]
vm.$options.components.__proto__[camelizedId]
vm.$options.components.__proto__[PascalCaseId]
从 resolveAsset
我们还能得到如下知识点:
- 当组件被定义为连字符时,我们只能在字符串模板使用
<my-comp><my-comp>
- 当组件被定义为驼峰时,我们可以在字符串模板使用
<my-comp><my-comp>
,<myComp><myComp>
- 当组件被定义为首字母大写时,我们可以在字符串模板使用
<my-comp><my-comp>
,<myComp><myComp>
,<MyComp><MyComp>
总结
注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options
下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components
扩展到当前组件的 vm.$options.components
下,这就是全局注册的组件能被任意使用的原因。