组件注册
在Vue.js中,除了它内置的组件如keep-alive
、component
、transition
、transition-group
等,其它用户自定义组件在使用前必须注册。在开发过程中可能会遇到如下报错信息:
'Unknown custom element: - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
一般报这个错的原因都是我们使用了未注册的组件。Vue.js提供了2种组件的注册方式,全局注册和局部注册。接下来我们从源码分析的角度来分析这两种注册方式。
全局注册
要注册一个全局组件,可以使用Vue.component(tagName, options)
。例如:
Vue.component('my-component', {
// 选项
})
那么,Vue.component
函数是在什么时候定义的呢,它的定义过程发生在最开始初始化Vue
的全局函数的时候,代码在src/core/global-api/assets.js
中:
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
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)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
函数首先遍历ASSET_TYPES
,得到type
后挂载到Vue
上。ASSET_TYPES
的定义在src/shared/constants.js
中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
所以实际上Vue
是初始化了3个全局函数,并且如果type
是component
且definition
是一个对象的话,通过this.opitons._base.extend
, 相当于Vue.extend
把这个对象转换成一个继承于Vue
的构造函数,最后通过this.options[type + 's'][id] = definition
把它挂载到Vue.options.components
上。
由于我们每个组件的创建都是通过Vue.extend
继承而来,我们之前分析过在继承的过程中有这么一段逻辑:
Sub.options = mergeOptions(
Super.options,
extendOptions
)
也就是说它会把Vue.options
合并到Sub.options
,也就是组件的options
上, 然后在组件的实例化阶段,会执行merge options
逻辑,把Sub.options.components
合并到vm.$options.components
上。
然后在创建vnode
的过程中,会执行_createElement
方法,我们再来回顾一下这部分的逻辑,它的定义在src/core/vdom/create-element.js
中:
export function _createElement (
context: Component,
tag?: string | Class | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array {
// ...
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
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 {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
// ...
}
这里有一个判断逻辑isDef(Ctor = resolveAsset(context.$options, 'components', tag))
,先来看一下resolveAsset
的定义,在src/core/utils/options.js
中:
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
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
}
这段逻辑很简单,先通过const assets = options[type]
拿到assets
,然后再尝试拿assets[id]
,这里有个顺序,先直接使用id
拿,如果不存在,则把id
变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使用Vue.component(id, definition)
全局注册组件的时候,id
可以是连字符、驼峰或首字母大写的形式。
那么回到我们的调用resolveAsset(context.$options, 'components', tag)
,即拿vm.$options.components[tag]
,这样我们就可以在resolveAsset
的时候拿到这个组件的构造函数,并作为createComponent
的钩子的参数。
局部注册
Vue.js也同样支持局部注册,我们可以在一个组件内部使用components
选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
其实理解了全局注册的过程,局部注册是非常简单的。在组件的Vue
的实例化阶段有一个合并option
的逻辑,之前我们也分析过,所以就把components
合并到vm.$options.components
上,这样我们就可以在resolveAsset
的时候拿到这个组件的构造函数,并作为createComponent
的钩子的参数。
注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到Vue.options
下,所以在所有组件创建的过程中,都会从全局的Vue.options.components
扩展到当前组件的 vm.$options.components
下,这就是全局注册的组件能被任意使用的原因。