重学Vue【组件注册原理分析】

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是本身的,全部文章都同步在 公众号(道道里的前端栈)github 上。前端

正文

在开发过程当中,自定义组件必须先注册才可使用,若是直接使用的话,会报一个错:未知的自定义元素,就像下面这样:vue

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

在vue中提供了2种组件注册的方式:全局注册局部注册,下面来把它们分析一下。node

全局注册

全局注册一个组件,通常会在 main.js 中这样写:git

Vue.component("comp-name", {
  // options
})
复制代码

使用了一个 Vue.component 函数来注册,这个函数的定义过程是在最开始初始化Vue的全局函数的时候,代码在 src/core/global-api/assets.js 中:github

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

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,往Vue上扩展了几个方法,每一个方法都有两个参数,一个id,一个自定义函数或对象,若是没有 definitioin,那就不日后走了,不然就继续。在后面的逻辑里,对组件名作了一层校验,后面若是 type 是一个组件,而且它的定义是一个普通对象,就把 name 赋值,接着用 this.options._base.extend(),把第二个参数转换成一个构造器, this.options._base 其实就是大Vue(以前分析过,经过 Vue.options._base = Vue 得知的),而后使用 Vue.extend() 把参数转化为构造器,最后把这个构造器赋值给 this.options[type + 's'][id] ,也就是给大Vue扩展定义了一个 components 构造器,最终挂载到了 Vue.options.components 上。api

因为在Vue初始化的时候会调用一个 _createElement 方法(它在render函数渲染和建立元素的时候分析过,能够看这篇createElement作了什么),在这个方法里有这样一段代码:markdown

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

注册组件会走到 vnode = createComponent(Ctor, data, context, children, tag) 逻辑中,就会建立一个组件vnode,能够看到调用了一个 resolveAssets 方法,传入了 vm.$optionscomponentstag,这个方法定义在 src/core/util/options.jside

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,以前在分析合并配置的时候(能够看这篇Vue的合并配置过程)有提到: vm.$options 实际上是自定义配置和大 Vue.options 一块儿合并出来的,因此在 asset.js 中的最后,给大Vue扩展一个 components,在 resolveAsset 第一个参数 options 上就能够去找,也就是下面的一些 if 判断了。函数

继续看,type 传入的是 components,赋值给 assets,而后判断 assets 自身有 id 属性的话,就返回它,不然就把 id 转化为驼峰,后面一样的逻辑,根据驼峰去找,若是驼峰找不到,就找首字母大写,若是仍是找不到,那么注释上写的,去原型上找,原型上找的顺序也是先 id,再驼峰,再首字母大写,若是还找不到,那这个vnode就是个空(或者说不认识这个vnode),也就会走到 _createElement 方法的后面判断,也就是经过 new 来建立一个VNode:oop

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

这也就是为何自定义全局组件的时候能够把 id 写成 驼峰,或者 首字母大写 的方式使用。

捋一下,若是是全局自定义组件,就会在大 Vue.options.components 里扩展了一个构造器,接着在初始化建立元素(_createElement)的时候,经过 resolveAsset 传入的 tag,解析出来一个有关组件标签的定义,而后返回这个构造器,把它传入到 createComponent 里去建立组件的vnode,而后走patch和update过程最终变成一个真实DOM。

局部注册

局部注册通常会在某个vue文件这样写:

<template>
	<Comp />
</template>
<script>
import Comp from "Comp.vue";
export default {
  components:{
    Comp
  }
}
</script>
复制代码

这样就引入了一个局部组件,如今来分析一下它的过程。

回顾一下 Vue.extend 是如何合并 options 的:

const Sub = function VueComponent (options) {
  this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
  Super.options,
  extendOptions
)
复制代码

Super.options 是大 Vue.$options,后面的 extendOptions 就是上面例子中的这一块:

export default {
  components:{
    Comp
  }
}
复制代码

把这两个合并到了子组件构造器的 options 上,就是 Sub.options 上,接着在这个Sub初始化的时候,会调用一个 initInternalComponent 方法(也就是调用 _init 里面的方法,代码在 /src/core/instance/init.js):

if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
复制代码

接着看下 initInternalComponent 这个方法:

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

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

里面的 vm.constructor 就是上面说的 Sub,这样 Sub.options 就能够拿到咱们在页面里写的组件配置,而后赋值给 vm.$options,因此能够经过 vm.$options.components 拿到页面里定义的组件配置,那在全局注册里的提到的 assets 就能够拿到这个局部注册的组件配置。

注意:因为局部组件的合并配置是扩展到 Sub.options 的,因此引入的这个局部组件只能在当前组件下使用(或者说当前vue页面),而全局注册是扩展到大 Vue.options 下的,也就是会走到 _init 方法的 vm.$options = mergeOptions() 这里:

if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
复制代码

因此能够全局使用。

个人公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感受真奇妙~

相关文章
相关标签/搜索