Skip to content

组件注册

一个vue组件在使用前需要先被注册,这样vue才能在渲染模板时找到其对应的实现。组件注册的类型有全局注册和局部注册。

全局注册

使用

要注册一个全局组件,可以使用 Vue.component(tagName, options)。

js
// 注册组件,传入一个扩展过的构造器, Vue.extend生成一个构造函数
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
  3. 相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。

源码分析

入口

首先是在initGlobalAPI 静态方法中调用initAssetRegisters()方法,这个在初始化部分说明过。

js
/* 注册Vue.directive(),Vue.component(), Vue.filter()*/
initAssetRegisters(Vue)
js

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 中:

js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

遍历这三种类型,Vue初始化三个全局函数。这里来分析Vue.component()函数的创建。

这段代码需要关注的部分是这里:

js
definition = this.options._base.extend(definition)
...
this.options[type + 's'][id] = definition

extend的作用就是创建组件,将组件的选项对象转化为一个组件构造函数,变成Vue构造函数的子类。最后通过 this.options[type + 's'][id] = definition 把它挂载到 Vue.options.components 上。

总结:这部分需要通过调试来理解 Vue.extend 函数根据传入的扩展选项创建一个 Sub 构造函数,原型继承于 Vue,然后合并扩展选项和 Vue.optionsSub.options,再处理 propscomputed 属性,扩展静态方法等。所以组件对象拥有和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

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的处理:

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
}

直接使用 idassets[id],如果不存在,则把 id 变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到就报错。

局部注册

使用

在一个组件内部使用 components 选项做组件的局部注册,例如:

js
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 中。这也是为什么需要缓存,