本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。
在 Vue.js 中,除了它内置的组件如 keep-alive、component、transition、transition-group 等,其它用户自定义组件在使用前必须注册。很多同学在开发过程中可能会遇到如下报错信息:
'Unknown custom element: <xxx> - 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 中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
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 {
...
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
}
}
})
}
所以实际上 Vue 是初始化了 3 个全局函数,并且如果 type 是 component 且 definition 是一个对象的话,通过 this.opitons._base.extend, 相当于 Vue.extend 把这个对象转换成一个继承于 Vue 的构造函数VueComponent,最后通过 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<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
let vnode, ns
if (typeof tag === 'string') {
let Ctor
// 如果是html标签则实例化一个vnode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果是一个组件,那么获取组件对象,进而生成组件的构造函数
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)
}
// ...
}
这里有一个判断逻辑 isDef(Ctor = resolveAsset(context.$options, 'components', tag)),先来看一下 resolveAsset 的定义,在 src/core/utils/options.js 中:
export function resolveAsset (options: Object,type: string, id: string..): any {
const assets = options[type]
// 在自身上查找对应的组件options
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]
// 从原型上找
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
...
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 函数的参数,最后生成组件的占位符vnode。
function createComponent() {
...
const baseCtor = context.$options._base;
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
);
}
也就是,全局注册的组件存放在Vue.options.components中,在生成组件的构造函数的时候把Vue里面的option合并到组件的option里面,这样就组件内部就有了全局注册的组件构造函数。
局部注册
Vue.js 也同样支持局部注册,我们可以在一个组件内部使用 components 选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
其实理解了全局注册的过程,局部注册是非常简单的。在组件的构造函数过程中,传入的options放在构造函数VueComponent上,然后在组件的 Vue 的实例化阶段有一个合并 option 的逻辑,之前我们也分析过,所以就把 VueComponent.options.components 合并到 vm.$options.components 上,这样我们就可以在 resolveAsset 的时候拿到这个组件对象,并作为 createComponent 的参数生成组件构造函数。
注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。
梳理组件创建过程
以如下代码为例,来完整梳理下整个组件的创建过程。
<template>
<div id="app">
<div>
{{ name }}
</div>
<my-test></my-test>
</div>
</template>
<script>
import MyTest from './components/MyTest.vue'
export default {
name: 'App',
components: {
MyTest
},
data() {
return {
name: 'pengchangjun'
}
}
}
</script>
-
首先通过
vue-loader编译器把template模板转化为render函数,render函数如下: -
在执行render函数的时候,执行
_c方法,该方法就是_createElement方法,先执行children里面的_c,最后执行外层的_c,最后生成的vnode如下:
children有两个vnode,一个是正常的html标签的vnode,还有一个组件vnode(占位符vnode)
- 最后执行
patch方法,递归调用createElm,如果是普通的vnode则生成dom,如果是组件vnode则再走一遍_init render patch方法 生成真实Dom。