组件注册
一个vue组件在使用前需要先被注册,这样vue才能在渲染模板时找到其对应的实现。组件注册的类型有全局注册和局部注册。
全局注册
使用
要注册一个全局组件,可以使用 Vue.component(tagName, options)。
// 注册组件,传入一个扩展过的构造器, Vue.extend生成一个构造函数
Vue.component('my-component', Vue.extend({ /* ... */ }))
// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })
// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
- 相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
源码分析
入口
首先是在initGlobalAPI 静态方法中调用initAssetRegisters()方法,这个在初始化部分说明过。
/* 注册Vue.directive(),Vue.component(), Vue.filter()*/
initAssetRegisters(Vue)
export function initAssetRegisters(Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
// definition是用户传入的
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
// 直接返回已注册的全局组件/指令/过滤器,在global-api.js中设置了存储
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)) {
// 确保有name属性
definition.name = definition.name || id
// 把组件配置转换成组件的构造函数
// this.options._base Vue构造函数
// 因为是静态方法,所以this.options._base.extend等价于调用 Vue.extend(definition),并把该返回值放入 Vue.options['components'] 对象中。
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 定义在 src/shared/constants.js 中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]遍历这三种类型,Vue初始化三个全局函数。这里来分析Vue.component()函数的创建。
这段代码需要关注的部分是这里:
definition = this.options._base.extend(definition)
...
this.options[type + 's'][id] = definitionextend的作用就是创建组件,将组件的选项对象转化为一个组件构造函数,变成Vue构造函数的子类。最后通过 this.options[type + 's'][id] = definition 把它挂载到 Vue.options.components 上。
总结:这部分需要通过调试来理解 Vue.extend 函数根据传入的扩展选项创建一个 Sub 构造函数,原型继承于 Vue,然后合并扩展选项和 Vue.options 为 Sub.options,再处理 props 和 computed 属性,扩展静态方法等。所以组件对象拥有和vue实例一样的成员。最后把 Vue.options、扩展选项等引用保存在 Sub 静态属性上,在后续实例化组件时用来检查更新选项的操作。在函数的最后返回 Sub 构造函数,在 Vue.component 中把它存入 Vue.options.components 对象中以完成全局注册。
全局注册的组件会在 Vue.options.components 中存储,不过在实例对象中vm.$options.components 也可获得。
创建vnode
在创建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
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拿到vm.$options.components[tag],也就是组件的构造函数,将作为createComponent的参数。 这里要额外提到的一点是,我们在注册组件时定义组件名时可以写kebab-case,也可以写PascalCase,都能够找到对应的资源,这是因为resolveAsset的处理:
/**
* 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
}直接使用 id 拿assets[id],如果不存在,则把 id 变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到就报错。
局部注册
使用
在一个组件内部使用 components 选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}源码分析
局部注册的组件的构造函数也就是 extend, 是在创建其对应的 Vnode对象时在函数 createComponent 中才创建的。在 createComponent 中也是通过调用 Vue.extend 创建的局部组件的构造函数,在createComponentInstanceForVnode执行 _init。把 components 合并到 vm.$options.components 上,这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数。
注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。
Quiz Time👇
如果创建组件的 Vnode 多次,那么构造函数也会重复创建吗?
✅ 答案是否定的,因为在 Vue.extend 中创建子类之后会把子类构造函数存入 Vue.extend 的实参 extendOptions._Ctor 中。这也是为什么需要缓存,