重學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
  )
}
複製代碼

因此能夠全局使用。

個人公衆號:道道里的前端棧,每一天一篇前端文章,嚼碎的感受真奇妙~

相關文章
相關標籤/搜索