vue2从数据到视图渲染:组件注册(全局组件/局部组件)

493 阅读2分钟

vue内部内置了很多好用的组件可以直接拿来使用,自己自定义的组件则需要先定义后使用。

组件注册可以分为全局注册和局部注册。

1、全局注册

import Vue from "vue";
import App from "./App";
// 全局注册
import globalComponent from "@/components/globalComponent";
Vue.component("globalComponent", globalComponent);

const app = new Vue({
  el: "#app",
  render(h) {
    return h(App);
  }
});

这里的globalComponent可以是手写的,也可以是引入并通过vue-loader处理过的,通过Vue.component("globalComponent", globalComponent)的方式注册的页面中所有的页面都可以直接使用

<template>
  <div>
    <globalComponent></globalComponent>
  </div>
</template>

那么Vue.component是什么时候定义的?
答:在initGlobalAPI(Vue)时定义的。
initGlobalAPI(Vue)中有方法initAssetRegisters(Vue):

import { ASSET_TYPES } from 'shared/constants'
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
      }
    }
  })
}

其中ASSET_TYPES在shared/constants.js中:

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

ASSET_TYPES通过循环的方式给Vue上分别挂载全局组件注册、全局指令和全局过滤器方法,这里主要看组件的注册,当符合type === 'component'并且definition是普通对象的时候,给将this.options._base.extend(definition)赋值给definitionthis.options._base指的是Vue,extend是将当前的组件转化成组件构造函数。可以参考juejin.cn/post/712909…
最后,this.options[type + 's'][id] = definition相当于Vue.options.components[组件ID] = definition
完成组件的注册后,在构造vNode阶段就可以使用了,在_createElement函数中有一段逻辑是:

if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      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 {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }

当前tagsting格式,在当前条件下,会执行到isDef(Ctor = resolveAsset(context.$options, 'components', tag)的判断,这里的context.$options是在组件构造函数实例化this._init执行初始化逻辑的方法initInternalComponent(vm, options)中进行赋值的:

function initInternalComponent (vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  // doing this because it's faster than dynamic enumeration.
  var parentVnode = options._parentVnode;
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;

  var vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}

var opts = vm.$options = Object.create(vm.constructor.options)将当前实例构造函数中的options赋值给实例的$options。 继续看isDef(Ctor = resolveAsset(context.$options, 'components', tag)),如果满足则会按照组件的方式创建vNode,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
}

options[type]就是options['components'],用来获取到当前options或者祖先链路上的components数组后再通过assets[id]assets[camelizedId]assets[PascalCaseId]的方式获取组件,即分别通过原始id、转换成小驼峰和大驼峰后的id去获取组件的构造函数。在全局注册的例子中,可以获取到assets[id],即可返回res,进入条件执行并vnode = createComponent(Ctor, data, context, children, tag)

2、局部注册

在组件App.vue中:

<template>
  <div>
    <partialComponent></partialComponent>
  </div>
</template>
<script>
import partialComponent from '@/components/partialComponent'
export default {
  components: {
    partialComponent,
  }
}
</script>

这里定义了components,在创建vNode的时候就可以通过const assets = options[type]获取components,然后通过if (hasOwn(assets, id)) return assets[id]判断条件存在并返回,进而进入条件执行并vnode = createComponent(Ctor, data, context, children, tag)

小结

全局组件一次注册在页面中可以到处使用,局部注册只在当前页面中可以用。一般使用率比较高的组件可以通过全局注册,只在当前页面中的业务组件可以通过局部注册完成。