Vue源码探秘(十三)(组件注册)

2020/4/30 11:02:41

本文主要是介绍Vue源码探秘(十三)(组件注册),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

引言

Vue.js 中,除了它内置的组件如 keep-alivecomponenttransitiontransition-group 外,其它用户自定义的组件在使用前必须注册,否则会抛出如下错误:

组件的注册有两种方式:全局注册局部注册。这里我们举两个例子:

// main.js

import Vue from "vue";
import App from "App.vue";

Vue.component("app", App);

new Vue({
  el: "#app",
  template: "<app></app>"
});
复制代码
// App.vue

<template>
  <div id="app">
    <Hello/>
  </div>
</template>
<script >
  import Hello from 'Hello.vue'
  export default {
    name: 'app',
    components: {
      Hello
    }
  }
</script>
复制代码

上面这两个例子中,第一个App组件是全局注册,第二个Hello组件是局部注册。

本小节,我会结合源码和上面举的两个例子分别分析这两种注册的实现。

全局注册

我们可以使用 Vue.compoment 来全局注册组件。所谓全局,就是注册出来的组件在任意一个创建出来的 Vue 实例中都可以使用。

Vue.component 是在哪里定义的呢,实际上是在 Vue 初始化时调用 initAssetRegisters 函数的时候。initAssetRegisters 定义在 src/core/global-api/assets.js 文件中:

// src/core/global-api/assets.js

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;
      }
    };
  });
}
复制代码

ASSET_TYPES 之前也见过多次了:

export const ASSET_TYPES = ["component", "directive", "filter"];
复制代码

现在我们只分析 ASSET_TYPEScomponent 的情况。这里的两个参数 iddefinition ,分别对应例子中的 appApp 组件对象

函数首先会对组件名做校验检查组件名是否合法。

然后判断如果 definition 参数是对象则做进一步处理:如果 definition 对象没有 name 属性,则将参数 id 作为 definition.name ,然后调用 this.options._base.extend(之前有介绍过,实际上就是 Vue.extend)把 definition 转换成一个继承自 Vue 的构造函数。

转换完成后将这个构造函数赋值给 this.options.components[id] ,最后将其返回。

执行完上述操作后,对应我们的例子就变为了:

Vue.options = {
  components: {
    app: function VueComponent() {}, // 全局注册的 App 组件
    keepAlive: {},
    Transition: {},
    TransitionGroup: {}
  },
  filters: {},
  directives: {},
  _base: function Vue() {}
};
复制代码

也就是说全局注册会把组件扩展到 Vue.options 下。

执行完 Vue.component ,代码执行到 new Vue 这条语句。经过前面学习,我们知道创建一个 Vue 实例时,就会执行 _init 函数,而 _init 函数其中有一步合并 options 的操作。

合并 options 有两种情况,在执行外部 new Vue 时会走 else 逻辑:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
);
复制代码

这里把 Vue.options 和外部编写的 options 一起合并到 vm.$options 上。

执行到这里,vm.$options 大概是这样的:

vm.$options = {
  el: "#app",
  template: "<app></app>",
  components: {
    __proto__: {
      // 继承自 Vue.options
      app: function VueComponent() {}, // 全局注册的 App 组件
      keepAlive: {},
      Transition: {},
      TransitionGroup: {}
    }
  },
  filters: {},
  directives: {},
  _base: function Vue() {}
};
复制代码

然后在创建 vnode 的过程中,会执行 _createElement 方法。回顾 _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 (
      (!data || !data.pre) &&
      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 if 逻辑,这个逻辑针对的是组件,它通过调用了 resolveAsset 函数来判断。来看 resolveAsset 函数的定义:

// src/core/util/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;
}
复制代码

我们一起来梳理下resolveAsset 函数的查询逻辑:

  • 检查assets[id] 是否存在,存在返回,不存在往下走
  • id 变成驼峰的形式再取,即检查assets[camelize(id)] 是否存在,存在返回,不存在继续往下走
  • 在驼峰的基础上把首字母再变成大写的形式再取,即检查 assets[capitalize(camelizedId)] 是否存在,存在返回,不存在抛出警告

这样我们就拿到了vm.$options.components[tag],即可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数。

局部注册

分析完全局注册,我们来看局部注册。

现在代码执行到创建 App 组件构造函数这里,回顾 Vue 源码探秘(九)(createComponent) , App 组件构造函数会通过 Vue.extend 来创建,其中有这么几行代码:

Sub.options = mergeOptions(Super.options, extendOptions);
复制代码

这里把 Vue.options 和我们在 App.vue 编写的 extendOptions 一起合并到 Sub.options 上 ,而 Sub 就是 App 组件的构造函数。

也就是说局部注册会把组件扩展到 Sub.options 下,另外也正是有这一步,Vue.options 会被合并到各个组件的构造函数 options 上,这也是为什么全局组件在任意一个地方都能使用。

之后就会调用 Sub 创建 App 组件实例,同样会执行 _init 函数,并且在合并 options 时会走 if 逻辑:

if (options && options._isComponent) {
  initInternalComponent(vm, options);
}
复制代码

此时它会调用 initInternalComponent 函数:

export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create(vm.constructor.options));

  // ...
}
复制代码

initInternalComponent 函数执行了 vm.$options = Object.create(vm.constructor.options) 这一步,这里的 vm 对应我们例子就是 App 组件实例vm.$options 就继承了 Sub.options

之后同样是调用 resolveAsset 函数,拿到这个组件的构造函数,并作为createComponent 的钩子的参数。

这样 resolveAsset 函数在 if (hasOwn(assets, id)) 逻辑判定就为 true ,直接就返回了 Hello 组件。之后的流程就不再赘述了。

总结

通过这一小节的分析,我们对两种组件的注册过程有了认识,并理解了全局注册局部注册的差异:

  • 全局注册的组件可以在任意地方使用,因为组件会通过 Vue.component 函数扩展到 Vue.options 上,而各个组件初始化时都会将 Vue.options 与自身 options 合并,这样每个组件都能访问到这个全局注册的组件。
  • 局部注册的组件只能在当前组件使用,因为组件仅仅只是扩展到 Sub.options 也就是当前组件构造函数的 options 上。


这篇关于Vue源码探秘(十三)(组件注册)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程