vue组件的全局注册和局部注册原理解析

4,770 阅读3分钟

vue组件的注册分为全局注册和局部注册,全局注册可以在任意组件中使用,而局部注册只能在当前组件中使用,接下来我们就来一探究竟吧。

全局注册

Vue.component('App', {
    render(h) { 
        return <h1>hello world</h1> 
    }
})

new Vue({
    render: h => h('App'), // 注意要使用字符串
}).$mount('#app')

使用了Vue.component()这个全局的方法,他的定义是在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)// Vue.extend()
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition VueComponent() {}
        return definition
      }
    }
  })
}

这样的方法还有filter、directive,我们只关注component,对应这一段代码

if (type === 'component' && isPlainObject(definition)) {
  definition.name = definition.name || id
  definition = this.options._base.extend(definition)// Vue.extend()
}
...
this.options[type + 's'][id] = definition

this就是Vue,this.options._base也是指Vue, 可以看到调用Vue.extend方法,传入一个对象返回一个构造函数,然后把这个构造函数放在了Vue.options.components.App中。这样就把全局注册的组件扩展到了Vue.options上了,然后我们怎么就能在任意地方使用呢?

先不着急,接着往下说,new Vue的时候就会调用用户传入的render方法去渲染vnode,然后执行core/vdom/create-element.js中的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
      )
    }
  }

我们传入的是字符串'App'并且不是保留标签,因此走到这一段

else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  // component
  vnode = createComponent(Ctor, data, context, children, tag)
} 

Ctor = resolveAsset(context.$options, 'components', tag)这一段就是找组件对应的构造函数, context就是vm根实例。接下来我们看看resolveAsset方法都做了哪些事。

它的定义是在core/util/options.js中:

export function resolveAsset (
  options: Object, // {components, directives, ...}
  type: string,// 'components'
  id: string,// 'App'
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type] // options就是vm.$options
  // 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,就是vm.$options,它是由Vue.options和用户传入的options合并而来的,这是在_init方法中做的:

 vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

mergeOptions中有很多的合并策略,我们重点关注component、directive、filter这些assets的合并策略:

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

...

for (key in parent) {
    mergeField(key)
}

...
function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
}

其中有这么一句:

const res = Object.create(parentVal || null)

针对Vue.options下的components、filters、directives都生成了一个空对象res, res.__proto__又指了回去,然后拿这个空对象和childVal合并,最终返回了res这个对象。

接着往下,通过assets = options[type]拿到assets,这个assets肯定是个空对象,然后尝试在当前组件本身上去获取assets[id],这里有一个顺序,先直接用id, 然后变成驼峰, 然后变成首字母大写,这就说明我们 全局注册组件的时候id可以是连字符、驼峰、或者首字母大写的形式, 如果都拿不到再去原型上找,也就是找全局组件。

找到组件对应的构造函数之后,下一步就是创建组件了

vnode = createComponent(Ctor, data, context, children, tag)

局部注册

import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}

相对全局注册来说就比较简单了,主要原理就是通过Vue.extend方法把上面导出的组件对象传入,然后创建这个组件对应的构造函数,注意为什么是对应的构造函数?因为每一个组件都有一个自己的构造函数,是一对一的关系。创建构造函数的过程中,会有以下逻辑:

 Sub.options = mergeOptions(// 父类的选项和子类的对象合并
      Super.options,
      extendOptions
    )

可以看到,父类Vue.options和上面传入的组件对象进行了合并,这样二者的Sub.options.components就包含了全局的和局部的。注意合并策略还是和上面的全局注册是一样的。

在组件初始化过程中会调用core/instance/index.js中的initInternalComponent方法

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

这里的vm就是组件实例,给组件实例的$options定义了一个空的对象,这个空的对象的原型__proto__指向了vm.constructor.options也就是Sub.options。

这样的话通过Sub生成的组件实例就可以访问到Super也就是Vue上定义的全局组件对应的构造函数和定义在自身上的局部组件对应的构造函数了。这部分逻辑也在resolveAsset方法中。

总结

全局注册的组件是扩展到了Vue.options下,所有组件的构造函数都会把Vue.options扩展到自己的options中,因此所有的组件都可以使用。而对于根实例vm也就new出来的那个实例,在_init的时候会把Vue.options合并到vm.$options中去。

全局注册的组件因为不属于任何一个组件,所以采用放在了vm.options.components的原型上这种合并策略, 简单说就是Vue.options.components给vm.$options.components用的时候,只给一个空对象,这个空对象指向我就好了,直接白给不可能的,哼哼~

而局部注册的组件是扩展到当前组件对应的构造函数上了,因此只有该组件才能使用。