【Vue源码】12.组件化-组件注册

255 阅读3分钟

组件化-组件注册

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 主要做了下面几件事:

  1. 遍历 ASSET_TYPES,得到 type 之后把 type 方法挂载到 Vue 实例上。所以是初始化了三个全局方法
  2. 调用 Vue.component ,即 typecomponent,并且 definition 是对象时
    • 重新赋值 definition.name
    • 通过 this.options._base.extend(即 Vue.extend )把 definition 对象转换为一个继承于 Vue 的构造函数
  3. 把转换后的构造函数挂载到 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;
}

这段逻辑的主要逻辑就是:

  1. 根据传进来的 typeoptions 里找然后赋值给 assets
  2. 根据传进来的 id 拿,如果有直接返回 assets[id]
  3. 第二步没拿到,把 id 转为驼峰的形式再拿
  4. 第三步没拿到,把驼峰转为首字母大写的形式再拿
  5. 如果都没有则再开发环境下报错

通过这段逻辑我们可以知道在使用组件的时候,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 中。对于一些通用的基础组件一般是使用全局注册的方式,而针对特例场景的组件通常会采用局部注册的方式来使用。了解这些会给我们在工作中使用全局组件还是局部组件很好的帮助。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘