组件化-组件注册
在 Vue 中,我们使用内置组件之外的组件必须先注册才可以使用,不然会报错:
[Vue warn]: Unknown custom element: <Test> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Vue 提供了两种注册组件的方式,全局注册和局部注册,接下来我们就来看一下这两种注册方式有什么不同以及他们的细节。
全局注册
通过 官网:全局注册 示例我们知道,可以使用 Vue.component('button-counter', options) 来注册一个全局组件。
Vue.component 是在初始化的过程中定义的,代码在 src/core/global-api/index.js 中:
/* @flow */
// ...
import { initAssetRegisters } from "./assets";
export function initGlobalAPI(Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null);
ASSET_TYPES.forEach((type) => {
Vue.options[type + "s"] = Object.create(null);
});
// ...
initAssetRegisters(Vue);
}
initAssetRegisters
initAssetRegisters 定义在 src/core/global-api/assets.js 中:
/* @flow */
export const ASSET_TYPES = ["component", "directive", "filter"];
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;
}
};
});
}
initAssetRegisters 主要做了下面几件事:
- 遍历
ASSET_TYPES,得到type之后把type方法挂载到Vue实例上。所以是初始化了三个全局方法 - 调用
Vue.component,即type为component,并且definition是对象时- 重新赋值
definition.name - 通过
this.options._base.extend(即Vue.extend)把definition对象转换为一个继承于Vue的构造函数
- 重新赋值
- 把转换后的构造函数挂载到
this.options.components,即Vue.options.components
由于我们的组件创建都是通过 Vue.extend 继承而来,在之前分析继承时有那么一段逻辑:
Sub.options = mergeOptions(Super.options, extendOptions);
他会把 Super.options( 即 Vue.options )合并到 Sub.options 上,也就是组件的 options 上。
然后再创建 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);
}
// ...
}
这段代码中有个判断逻辑 if (isDef(Ctor = resolveAsset(context.$options, 'components', tag)),符合条件则创建组件。
resolveAsset
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;
}
这段逻辑的主要逻辑就是:
- 根据传进来的
type去options里找然后赋值给assets - 根据传进来的
id拿,如果有直接返回assets[id] - 第二步没拿到,把
id转为驼峰的形式再拿 - 第三步没拿到,把驼峰转为首字母大写的形式再拿
- 如果都没有则再开发环境下报错
通过这段逻辑我们可以知道在使用组件的时候,id 可以是字符串、驼峰或者首字母大写的形式。
这段代码执行完如果找到组件的话返回组件的构造函数,然后执行 vnode = createComponent(Ctor, data, context, children, tag),
那么 Ctor 就是 组件的构造函数。
局部注册
在 官网:局部注册 示例我们知道,可以使用如下来注册一个局部组件:
var ComponentA = {
/* ... */
};
var ComponentB = {
/* ... */
};
var ComponentC = {
/* ... */
};
new Vue({
el: "#app",
components: {
ComponentA,
ComponentB,
},
});
在了解全局注册组件过程的前提下,局部注册也很简单。在 Vue 组件的实例化阶段会进行一个合并 options 的操作,也就是把 components 合并到 vm.$options.components 上(10.组件化-合并配置 中有讲),这样我们就可以 resolveAsset 时拿到组件的构造函数
总结
通过这一节,我们知道了组件的全局注册和局部注册的区别。全局注册的组件会放在 vm.$options.components 里面,所有的组件都会继承 vm,所以可以全局使用,局部注册组件只在自己的 $options.components 中。对于一些通用的基础组件一般是使用全局注册的方式,而针对特例场景的组件通常会采用局部注册的方式来使用。了解这些会给我们在工作中使用全局组件还是局部组件很好的帮助。
参考:Vue.js 技术揭秘