vue的源码学习之六——5.组件注册
1 介绍 版本:2.5.17。 我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。 学习文档:https://ustbhuangyi.github.io/vue-analysis/components/component-register.html2 组件注册在 Vue.js 中,除了它内置的组件如 keep-a...
1 介绍
版本:2.5.17。
我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。
学习文档:https://ustbhuangyi.github.io/vue-analysis/components/component-register.html
2 组件注册
在 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 种组件的注册方式,全局注册和局部注册
2.1 全局注册
使用Vue.components
实现全局注册
Vue.component('my-component', {
// 选项
})
2.1.1 Vue.component
Vue.components函数
定义过程发生在最开始初始化 Vue 的全局函数的时候,代码在 src/core/global-api/assets.js
中:
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string, // id是字符串
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
// 通过this.options._base.extend方法将定义对象转化为构造器,也就是Vue.extend方法
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 将构造器赋值给 this.options[‘component’+ 's'][id]
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
继承而来,我们之前分析过在继承的过程中有这么一段逻辑:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
let vnode, ns
// 如果tag是一个string
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 如果tag是一个保留标签也就是HTML标签的话
if (config.isReservedTag(tag)) {
// 创建一个普通VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 否知如果满足这个条件,就创建组件VNode,判断的时候回调用resolveAsset方法
} 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 {
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, // vm.$options,上买说了,因为options的合并,组件definitions构造器可以通过vm.$options访问
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type] // 其实就是组件definitions构造器
//
// 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其实就是组件definitions构造器,然后再尝试拿 assets[id],这里有个顺序,先直接使用 id 拿,如果不存在,则把 id 变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。
这样说明了我们在使用 Vue.component(id, definition) 全局注册组件的时候,id 可以是连字符、驼峰或首字母大写的形式。
那么回到我们的调用 resolveAsset(context.$options, 'components', tag),即拿 vm.$options.components[tag],这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数,创建组件VNode
2.1.2总结
- 通过assets.js,将组件的definition定义对象通过 this.opitons._base.extend, 相当于 Vue.extend 将其转换成一个继承于 Vue 的构造函数, 并this.options[type + 's'][id] =definition挂载到 Vue.options.components 上。
- 组件的创建都是通过 Vue.extend 继承,进而通过mergeOptions,使得每一个组件都拥有了options.components
- 在创建 vnode 的过程中,会执行 _createElement 方法,在该方法中利用resolveAsset判断该组件有没有vm.options.components对应的组件的definition定义
- 如果有组件的definition定义,就会创建组件VNode
2.2 局部注册
Vue.js 也同样支持局部注册,我们可以在一个组件内部使用 components 选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
其实是在构造子组件的构造器Sub的时候将 Super.options
和 extendOptions
做了一次合并。extendOptions
就是我们定义组件的对象,对于上面的代码,对应的就是
export default {
components: {
HelloWorld
}
}
在初始化的阶段src/core/instance/init.js中执行
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
}
vm.$options 就可以拿到Sub.options 配置。所以就可以vm.$options.components 拿到子组件,接着走创建VNode的函数,这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数。,创建组件VNode
2.2.1总结
- 组件的创建都是通过 Vue.extend 继承,Super.options和 extendOptions做了一次合并,使得该组件有了 extendOptions的options.components
- 在创建 vnode 的过程中,会执行 _createElement
- 方法,在该方法中利用resolveAsset判断该组件有没有vm.options.components对应的组件的definition定义
- 如果有组件的definition定义,就会创建组件VNode
3 总结
局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)