美文网首页
Vue源码分析—组件化(五)

Vue源码分析—组件化(五)

作者: oWSQo | 来源:发表于2019-07-17 13:28 被阅读0次

    组件注册

    在Vue.js中,除了它内置的组件如keep-alivecomponenttransitiontransition-group等,其它用户自定义组件在使用前必须注册。在开发过程中可能会遇到如下报错信息:

    'Unknown custom element: <xxx> - did you register the component correctly?
     For recursive components, make sure to provide the "name" option.'
    

    一般报这个错的原因都是我们使用了未注册的组件。Vue.js提供了2种组件的注册方式,全局注册和局部注册。接下来我们从源码分析的角度来分析这两种注册方式。

    全局注册

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

    Vue.component('my-component', {
      // 选项
    })
    

    那么,Vue.component函数是在什么时候定义的呢,它的定义过程发生在最开始初始化Vue的全局函数的时候,代码在src/core/global-api/assets.js中:

    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,得到type后挂载到Vue上。ASSET_TYPES的定义在src/shared/constants.js中:

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

    所以实际上Vue是初始化了3个全局函数,并且如果typecomponentdefinition是一个对象的话,通过this.opitons._base.extend, 相当于Vue.extend把这个对象转换成一个继承于Vue的构造函数,最后通过this.options[type + 's'][id] = definition把它挂载到Vue.options.components上。

    由于我们每个组件的创建都是通过Vue.extend继承而来,我们之前分析过在继承的过程中有这么一段逻辑:

    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    

    也就是说它会把Vue.options合并到Sub.options,也就是组件的options上, 然后在组件的实例化阶段,会执行merge options逻辑,把Sub.options.components合并到vm.$options.components上。

    然后在创建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)
      }
      // ...
    }
    

    这里有一个判断逻辑isDef(Ctor = resolveAsset(context.$options, 'components', tag)),先来看一下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
    }
    

    这段逻辑很简单,先通过const assets = options[type]拿到assets,然后再尝试拿assets[id],这里有个顺序,先直接使用id拿,如果不存在,则把id变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使用Vue.component(id, definition)全局注册组件的时候,id可以是连字符、驼峰或首字母大写的形式。

    那么回到我们的调用resolveAsset(context.$options, 'components', tag),即拿vm.$options.components[tag],这样我们就可以在resolveAsset的时候拿到这个组件的构造函数,并作为createComponent的钩子的参数。

    局部注册

    Vue.js也同样支持局部注册,我们可以在一个组件内部使用components选项做组件的局部注册,例如:

    import HelloWorld from './components/HelloWorld'
    
    export default {
      components: {
        HelloWorld
      }
    }
    

    其实理解了全局注册的过程,局部注册是非常简单的。在组件的Vue的实例化阶段有一个合并option的逻辑,之前我们也分析过,所以就把components合并到vm.$options.components上,这样我们就可以在resolveAsset的时候拿到这个组件的构造函数,并作为createComponent的钩子的参数。

    注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到Vue.options下,所以在所有组件创建的过程中,都会从全局的Vue.options.components扩展到当前组件的 vm.$options.components下,这就是全局注册的组件能被任意使用的原因。

    相关文章

      网友评论

          本文标题:Vue源码分析—组件化(五)

          本文链接:https://www.haomeiwen.com/subject/fxxshctx.html