Vue源碼探究-類初始化函數詳情

Vue源碼探究-類初始化函數詳情

隨着初始化函數的執行,實例的生命週期也開始運轉,在初始化函數裏可以看到每個模塊向實例集成的功能,這些功能的具體內容以後在單獨的文章裏繼續探索。現在來詳細看看類初始化函數的詳細代碼。

頭部引用

*下面代碼位於vue/src/core/instance/init.js

import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'

頭部注入的一些方法是在生命週期運行中開始初始化的功能,之前在覈心類實現的文章中有提到過,在這裏不展開。config對象是作爲基本的配置參數,在不同運行環境裏會更改當中的屬性值來適應不同的平臺需求,在這個文件中只用到了其中的性能檢測屬性,與具體的類的實現沒有太大關聯,與引入的markmeasureformatComponentName方法配合主要是做性能評估用的。

在初始化組件的時候主要用到的是工具方法extendmergeOptions

輔助函數extend

extend函數是一個很簡單的爲對象擴展屬性的方法,代碼位於這個文件中vue/src/shared/util.js,具體實現非常基礎,看看就好。

/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

輔助函數mergeOptions

mergeOptions函數代碼位於
vue/src/core/util/options.js中,它是初始化合並options對象時非常重要的函數,爲了看明白它在初始化函數裏的用途,稍微花點時間來仔細看一下它的具體實現。

// 該函數用於將兩個配置對象合併爲一個新的配置對象,
// 核心實體既用於實例化也用於繼承
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
// 導出mergeOptions函數
// 接收Object類型的parent、child參數,Component類型的vm參數
// 函數返回對象
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 非生產環境時檢查child對象的components屬性中是否有不合適的引用組件名稱
  // 不合適的組建名主要是指與Vue內建html標籤或保留標籤名相同的組件名稱如slot,component
  // 有興趣瞭解的可以參照同一文件中的L246到L269查看具體實現
  // 其中的輔助工具函數位於src/shared/util.js的L94到L112
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  // 如果child傳入的是函數對象,則將函數的options屬性賦值給child,確保child引用options
  if (typeof child === 'function') {
    child = child.options
  }

  // 下面三個函數都是將child的各個屬性格式化成預定好的對象格式
  // 標準化屬性
  normalizeProps(child, vm)
  // 標準化注入
  normalizeInject(child, vm)
  // 標準化指令
  normalizeDirectives(child)
  // 定義擴展
  const extendsFrom = child.extends
  // 如果存在則向下遞歸合併
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  // 如果存在mixins,則合併每一個mixin對象
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  // 定義以空對象options
  const options = {}
  let key
  // 對每一個parent中的屬性進行合併,添加到options中
  for (key in parent) {
    mergeField(key)
  }
  // 如果parent中不含有key屬性,則對每一個child中key屬性進行合併
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // 定義mergeField函數,接收key參數
  function mergeField (key) {
    // 如果strats[key]有定義好的合併策略函數,則複製給strat
    // 否則將默認的defaultStrat方法賦給strat
    const strat = strats[key] || defaultStrat
    // 合併屬性
    options[key] = strat(parent[key], child[key], vm, key)
  }
  // 返回最終options對象
  return options
}

儘管 mergeOptions 函數的實現有些複雜,但它的作用其實比較明確,就是解決初始化的過程中對繼承的類的options對象和新傳入的options對象之間同名屬性的衝突,即使用繼承的屬性值還是新傳入的屬性值的問題。在代碼的一開始官方就已說明它是一個遞歸函數,可以一併解決添加了擴展內容和使用了mixins的場景,總而言之,這個步驟就是確保我們初始化的實例的options對象正確唯一。

代碼中有幾個標準化屬性的函數,具體實現也在以上代碼的同一文件中,雖然有一堆代碼,但實現還是比較簡單,主要目的就是把傳入的options對象的各個屬性格式化成基於對象的預定格式,在以後的運行中方便使用。

hasOwn 函數是對 Object.prototype.hasOwnProperty 方法的一個包裝,比較簡單,需要了解的話就去util工具函數文件中查看。

值得一提的是 strats 的使用。在代碼的一開始的部分就定義 strats 變量,並說明它是用來處理父子選項合併屬性的功能。

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const strats = config.optionMergeStrategies

對於 elpropsData 屬性的合併策略賦予 defaultStrat 函數,該函數的原則是child對象屬性優先,沒有child對象屬性則返回parent的對應屬性。

/**
 * Options with restrictions
 */
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}

datawatchpropsmethodsinjectcomputedprovide 、各種鉤子函數和ASSET_TYPES裏包含的componentdirectivefilter 三個屬性都分別定義了相關的合併方法,有興趣繼續瞭解的同學可以在同一分文件中查看,代碼太長但是實現比較基礎,所以沒什麼好詳說的,可以關注一下的是某些屬性是替換覆蓋,而某些屬性是合併成數組如各種鉤子的監聽函數。

初始化內部組件時options的合併

對於初始化合並options的操作分爲了兩個方向,一是創建內部組件時的合併,二是創建非內部組件實例時的合併,先來說說內部組件初始化的詳細內容,在類的實現中對應着這一句代碼 initInternalComponent(vm, options)

// 輸出initInternalComponent函數
// 接受Component類型的vm參數和InternalComponentOptions類型的options參數
// 這裏vm和options分別是創建實例時將要傳入的實例對象和配置對象
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
 // 定義opts,爲opts和vm.$options創建以vm.constructor.options爲原型的對象
 const opts = vm.$options = Object.create(vm.constructor.options)
 // 以下爲手動賦值,目的爲了提升性能,因爲比通過動態枚舉屬性來賦值的過程快
 // doing this because it's faster than dynamic enumeration.
 // 定義父虛擬節點parentVnode並賦值
 const parentVnode = options._parentVnode
 // 設置opts對象parent和_parentVnode屬性
 opts.parent = options.parent
 opts._parentVnode = parentVnode

 // 定義vnodeComponentOptions並賦值
 const vnodeComponentOptions = parentVnode.componentOptions
 // 定義opts各屬性
 opts.propsData = vnodeComponentOptions.propsData
 opts._parentListeners = vnodeComponentOptions.listeners
 opts._renderChildren = vnodeComponentOptions.children
 opts._componentTag = vnodeComponentOptions.tag

 // options.render屬性存在,則設置opts的render和staticRenderFns屬性
 if (options.render) {
   opts.render = options.render
   opts.staticRenderFns = options.staticRenderFns
 }
}

可以看出 initInternalComponent 函數的內容比較簡單,主要是爲創建的內部組件的options對象手動賦值,提升性能,因爲按照官方註釋的說法是所有的內部組件的初始化都沒有列外可以同樣處理。至於什麼時候會創建內部組件,這種場景目前還不太瞭解,能確定的是通常創建Vue實例來初始化視圖頁面的用法是非內部組件性質的。在這裏留下一個疑問。

初始化實例時options的合併

下面三個函數就是初始化實例合併options這條線時用到的方法,後兩個函數作輔助用。對應如下帶代碼。主要是用來解決構造函數的默認配置選項和擴展選項之間的合併問題。

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
// 導出resolveConstructorOptions函數,接受Component構造函數Ctor參數
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // 定義傳入的構造函數的options屬性
  let options = Ctor.options
  // 如果Ctor.super存在則執行下面代碼,這裏是用來判斷實例對象是否有繼承
  // 如果有的話遞歸的把繼承的父級對象的options都拿出來合併
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // 如果父級的option變化了則要更新
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // 檢查是否有任何後期修改/附加選項,這是爲了解決之前誤刪注入選項的問題
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 如果返回有修改的選項,則擴展Ctor.extendOptions
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // 合併繼承選項和擴展選項
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      // 設置options.components[options.name]的引用
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  // 返回options
  return options
}

// 以下兩個函數是爲了解決#4976問題的方案
// 定義resolveModifiedOptions函數,接受Ctor參數,返回Object
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  // 定義modified變量儲存最終要選擇保留的屬性
  let modified
  // 分別定義最新、擴展和密封的配置選項
  const latest = Ctor.options
  const extended = Ctor.extendOptions
  const sealed = Ctor.sealedOptions
  // 遍歷傳入的配置選項對象
  for (const key in latest) {
    // 如果最新的屬性與密封的屬性不相等,則執行去重處理
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = dedupe(latest[key], extended[key], sealed[key])
    }
  }
  // 返回modified
  return modified
}

// 定義dedupe函數,接收latest最新對象,extended擴展對象,sealed密封對象
function dedupe (latest, extended, sealed) {
  // 合併選項的時候比較最新和密封的屬性,確保生命週期鉤子不重複
  // compare latest and sealed to ensure lifecycle hooks won't be duplicated
  // between merges
  // 如果latest是數組
  if (Array.isArray(latest)) {
    // 定義res變量
    const res = []
    // 格式化sealed和extended爲數組對象
    sealed = Array.isArray(sealed) ? sealed : [sealed]
    extended = Array.isArray(extended) ? extended : [extended]
    for (let i = 0; i < latest.length; i++) {
      // 返回擴展的選項中存在的最新對象的屬性而非密封選項以排除重複選項
      // push original options and not sealed options to exclude duplicated options
      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
        res.push(latest[i])
      }
    }
    // 返回包含了擴展選項的數組變量
    return res
  } else {
    // 否則直接返回latest
    return latest
  }
}

使用 resolveConstructorOptions 函數解決了繼承的構造函數的選項之後,新創建的實例vm的$options對象就是繼承選項和創建時傳入的options選項的合併。其中雖然有很多複雜的遞歸調用,但是這些函數的目的都是爲了確定最終的選項,理解這個目的非常重要。


初始化函數的執行不僅在於開始生命週期的運行,對於options對象的各個屬性值如何取捨的問題給出了非常複雜但健全的解決方法,這爲生命週期正常運行鋪墊了非常堅實的基礎,有了清晰的options選項,之後的功能才能如期順利執行。在這裏也可以看出Vue處理各種屬性的合併原則,對此有良好的理解可以確保在使用時立即定位遇到的相關問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章