组件注册分为两种形式,全局注册和局部注册,在分析组件注册之前,有必要先了解下initGlobalAPI方法
# initGlobalAPI
initGlobalAPI方法定义在src/core/global-api/index.js中,该方法在实例化Vue之前被调用
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
该方法逻辑较简单,主要是在Vue上注册一些全局API,其中Vue.use、Vue.mixin、Vue.extend、Vue.component分别通过调用initUse、initMixin、initExtend、initAssetRegisters方法进行全局注册,参数合并需要用到的Vue.options也是在该方法内定义的
# 全局注册
全局注册需要使用Vue.component方法,在src/shared/constants.js中,可以查看对该方法的定义
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
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
initAssetRegisters方法在initGlobalAPI方法中调用,在该方法内会循环遍历ASSET_TYPES中的每一项,动态的在Vue上挂载全局API,ASSET_TYPES在src/shared/constants.js中定义
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
2
3
4
5
遍历结束完成对Vue.component、Vue.directive、Vue.filter全局方法的注册
全局注册一个组件时会传递两个参数
- 组件名称
- 组件选项对象
在使用Vue.component方法进行全局组件注册时在方法内部对Vue.extend进行了调用,可以查看在createComponent (opens new window)小节对Vue.extend的分析,Vue.extend会根据传入的组件对象返回一个继承了Vue原型对象的构造函数,然后将返回的构造函数添加在Vue.options.components中,并返回该构造函数,Vue.directive、Vue.filter和Vue.component共用的一个函数体,逻辑较简单,相信大家可以看懂
# 局部注册
在Vue中我们可以在一个组件或实例内注册另一个组件,比如:
<body>
<div id="app">
<el-input/>
</div>
</body>
<script>
const vm = new Vue({
el: '#app',
components: {
'el-input': {
template: `<input :value="name" />`,
data () {
return {
name: 'zhangsan'
}
}
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在分析前可以先回顾下参数合并 (opens new window)小节
在实例化vm过程中,会进行参数合并 (opens new window),不同选项有不同的合并策略,在对components进行合并时会使用到mergeAssets方法,该方法定义在src/core/util/options.js中
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
该方法首先以parentVal(其实就是Vue.options.components)为原型创建res对象,再浅拷贝局部注册的组件并添加到res中
# 组件查找
组件注册完,使用组件有两种方式
- 在
template模板内直接使用,例如<el-input v-model="name"></el-input> - 使用渲染函数形式
vm.$createElement('el-input')
在分析new Vue过程 (opens new window)时讲过,template模板形式最终都会被编译为渲染函数形式,所以最终都是直接调用vm.$createElement('el-input')方法,也就是直接调用_createElement方法,可以在createComponent (opens new window)小节和render (opens new window)小节查看为什么会直接调用_createElement方法的分析,这里就不重复说明了,再次回到_createElement方法
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ......
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
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
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 组件查询
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)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
该方法内部代码可能看上去较多,但是逻辑较清晰,主要分析主线逻辑,会先规范化children,生成children vnode,接着会对tag做字符串判断,组件注册后,使用一个组件最终会编译为渲染函数的形式并根据组件名称进行查找,所以tag会是一个组件名字符串,由于组件名规范的问题不能是一个内置元素,所以会直接调用resolveAsset方法进行查询,resolveAsset方法定义在src/core/util/options.js中,
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
该方法主要是在assets中查找组件名为id的构造函数,在查询的过程中,对组件名称做了几次转换处理,这样做的目的主要是为了解决如下情况
- 比如我注册一个组件名称为
MyComponent,那么在使用时可以这样使用<my-component />,也可以这样<MyComponent>,或者渲染函数的形式vm.$createElement('MyComponent')或者vm.$createElement('my-component') - 如果组件名称为
my-component,那么在使用时只能这样使用<my-component />,或者渲染函数vm.$createElement('my-component')
在Vue官方文档组件名大小写 (opens new window)一节也有说明
resolveAsset方法会返回查找到的对应组件名称的构造函数,将返回的构造函数作为参数调用createComponent方法创建组件vnode,也可以叫占位符vnode(源码标注的名称为占位符vnode),个人习惯叫组件vnode