1. 程式人生 > >vue2.x原始碼解析六——元件化--6.元件註冊

vue2.x原始碼解析六——元件化--6.元件註冊

1.元件註冊

在 Vue.js 中,除了它內建的元件如 keep-alive、component、transition、transition-group 等,其它使用者自定義元件在使用前必須註冊。如果不註冊報錯資訊:

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

元件註冊有兩種方式:全域性註冊 和 區域性註冊

1.1 全域性註冊

使用Vue.components

實現全域性註冊

import App from './App'

Vue.components('app', APP);

new Vue({
  el: '#app',
  template: '<App/>'
})

1.2 區域性註冊

利用components物件

import hello from './components/HelloWorld'
export default {
  name: 'App',
  components: {
    hello
  }
}

2. 原始碼分析

2.1 全域性註冊實現

要註冊一個全域性元件,可以使用 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) {

  ASSET_TYPES.forEach(type
=> { Vue[type] = function ( id: string, // id是字串 definition: Function | Object // 定義是函式或者物件 ): Function | Object | void { // 如果沒有傳定義 if (!definition) { return this.options[type + 's'][id] } else { if (process.env.NODE_ENV !== 'production' && type === 'component') { validateComponentName(id) } // 如果是component(元件)方法,並且定義是物件 if (type === 'component' && isPlainObject(definition)) { //定義物件的name如果沒有的話就以ID為主 definition.name = definition.name || id // 通過this.options._base.extend方法將定義物件轉化為構造器,也就是Vue.extend方法 definition = this.options._base.extend(definition) } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition } } // 將構造器賦值給 this.options[‘component’+ 's'][id] 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 個全域性函式,並且如果 type 是 componentdefinition (定義)是一個物件的話,通過 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,也就是元件的 optinons 上, 然後在元件的例項化階段,會執行 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
  // 如果tag是一個string
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 如果tag是一個保留標籤也就是HTML標籤的話
    if (config.isReservedTag(tag)) {
      // 建立一個普通VNode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
       // 否知如果滿足這個條件,就建立元件VNode,判斷的時候回撥用resolveAsset方法
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {

      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    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, // vm.$options,上買說了,因為options的合併,元件definitions構造器可以通過vm.$options訪問
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type] // 其實就是元件definitions構造器
  // 
  // 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其實就是元件definitions構造器,然後再嘗試拿 assets[id],這裡有個順序,先直接使用 id 拿,如果不存在,則把 id 變成駝峰的形式再拿,如果仍然不存在則在駝峰的基礎上把首字母再變成大寫的形式再拿,如果仍然拿不到則報錯。
這樣說明了我們在使用 Vue.component(id, definition) 全域性註冊元件的時候,id 可以是連字元、駝峰或首字母大寫的形式。

那麼回到我們的呼叫 resolveAsset(context.$options, 'components', tag),即拿 vm.$options.components[tag],這樣我們就可以在 resolveAsset 的時候拿到這個元件的建構函式,並作為 createComponent 的鉤子的引數,建立元件VNode

總結

  1. 通過assets.js,將元件的definition定義物件通過 this.opitons._base.extend, 相當於
    Vue.extend 將其轉換成一個繼承於 Vue 的建構函式,
    this.options[type + 's'][id] =definition掛載到 Vue.options.components 上。

  2. 元件的建立都是通過 Vue.extend
    繼承,進而通過mergeOptions,使得每一個元件都擁有了options.components

  3. 在建立 vnode 的過程中,會執行 _createElement
    方法,在該方法中利用resolveAsset判斷該元件有沒有vm.options.components對應的元件的definition定義
  4. 如果有元件的definition定義,就會建立元件VNode

2.2區域性註冊

Vue.js 也同樣支援區域性註冊,我們可以在一個元件內部使用 components 選項做元件的區域性註冊,例如:

import HelloWorld from './components/HelloWorld'

export default {
  components: {
    HelloWorld
  }
}

其實是在構造子元件的構造器Sub的時候將 Super.optionsextendOptions做了一次合併。

extendOptions就是我們定義元件的物件,對於上面的程式碼,對應的就是

export default {
  components: {
    HelloWorld
  }
}

在初始化的階段src/core/instance/init.js中執行

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

vm.$options 就可以拿到Sub.options 配置。

所以就可以vm.$options.components 拿到子元件,接著走建立VNode的函式,這樣我們就可以在 resolveAsset 的時候拿到這個元件的建構函式,並作為 createComponent 的鉤子的引數。,建立元件VNode

總結

1.元件的建立都是通過 Vue.extend 繼承,Super.optionsextendOptions做了一次合併,使得該元件有了 extendOptionsoptions.components
2. 在建立 vnode 的過程中,會執行 _createElement
方法,在該方法中利用resolveAsset判斷該元件有沒有vm.options.components對應的元件的definition定義
3. 如果有元件的definition定義,就會建立元件VNode

總結

區域性註冊和全域性註冊不同的是,只有該型別的元件才可以訪問區域性註冊的子元件,而全域性註冊是擴充套件到 Vue.options 下,所以在所有元件建立的過程中,都會從全域性的 Vue.options.components 擴充套件到當前元件的 vm.$options.components 下,這就是全域性註冊的元件能被任意使用的原因。