Vue的mergeOptions函數分析-上

Vue的mergeOptions函數的主要做用是用於合併選項(將倆個選項對象合併成一個),它是用於實例化和繼承的核心函數。這也是爲何咱們要去分析它。而且與函數相關的選項合併策略也都在一個文件裏,定義在/src/core/util/options.js文件中。javascript

使用場景

由於Vue的核心代碼都是放在src文件夾下,因此咱們能夠在src目錄下全局搜索下mergeOptions的使用場景,能夠發現函數在Vue.extendVue.mixin實例化都有用到。(只考慮web平臺)html

// src/core/global-api/extend.js文件中
Vue.extend = function (extendOptions: Object): Function {
  // ... 忽略無關代碼
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
}

// src/core/global-api/mixin.js文件中
Vue.mixin = function (mixin: Object) {
  this.options = mergeOptions(this.options, mixin)
  return this
}

// src/core/instance/init.js文件中 執行new 實例化的時候會執行
Vue.prototype._init = function (options?: Object) {
  // ... 忽略無關代碼
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
複製代碼

這也證明了mergeOptions函數的註釋所寫的同樣,Core utility used in both instantiation and inheritance.vue

逐行分析

mergeOptions函數被定義在/src/core/util/options.js文件中,源代碼以下:html5

/** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
複製代碼

咱們先看函數接受的參數,這裏有一點過要注意,mergeOptions函數第三個參數的可選的,能夠不傳。Vue.mixinVue.extend函數中調用mergeOptions的時候是不傳第三個參數的。選項的合併策略函數會根據vm參數來肯定是實例化選項合併仍是繼承選項合併,從而作不一樣的處理,這個後面會詳細講到。java

函數第一行,檢查非生產環境下,執行checkComponents函數,該函數定義在同一文件下,主要是檢查組件的名字是否符合規範。能夠看到核心函數是validateComponentName,並且它被暴露出去,由於在Vue.component()Vue.extend()函數中都有用到。web

if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}

/** * 檢驗組件的名字 */
function checkComponents (options: Object) {
  // 遍歷對象的components屬性,依次檢驗
  for (const key in options.components) {
    validateComponentName(key)
  }
}
// 若是檢驗不經過,給出相應警告
export function validateComponentName (name: string) {
  // 符合HTML5規範,由普通字符和中橫線(-)組成,而且必須以字母開頭。
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  // isBuiltInTag是檢驗名字不能與slot,component重名
  // isReservedTag是檢驗不能與html、svg內置標籤重名
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}
複製代碼

接下來是檢查傳入的child是不是函數,若是是的話,取到它的options選項從新賦值給child。因此說child參數能夠是普通選項對象,也能夠是Vue構造函數和經過Vue.extend繼承的子類構造函數。(Vue.options定義在src/core/global-api/index.js文件中)api

if (typeof child === 'function') {
  child = child.options
}
複製代碼

再日後看有三個函數,分別是normalizePropsnormalizeInjectnormalizeDirectives,它們的做用是規範化選項,用過Vue的同窗應該都知道,咱們在寫propsinject既能夠是字符串數組,也能夠是對象。directives既能夠是一個函數,也能夠是對象。Vue對外提供了便捷的寫法,但內部處理要把他們規範成同樣,才更方便處理。其實三個函數都是將選項轉換對象的形式,接下來咱們會逐個分析。數組

規範化props

function normalizeProps (options: Object, vm: ?Component) {
  // 定義props,是選項中的props屬性的引用
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  // 1. 是數組的狀況 例如:['name', 'age']
  if (Array.isArray(props)) {
    i = props.length
    // 循環遍歷變成對象格式{ type: null }
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val) // 將key值變成駝峯形式
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        // 若是不是字符串數組,非生產環境給出警告
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    // 2. 是對象
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      // 若是是對象,則直接賦值,不是的話,則賦值type屬性
      // 例如 { sex: String, job: { type: String, default: 'xxx' } }
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    // 不是數組和對象給出警告
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  // 規範後結果賦值給options.props
  options.props = res
}
複製代碼

normalizeProps函數仍是比較簡單的,如上圖。當傳入是字符串數組時(例如: ['name', 'age']),說明只指定了key值,只須要將數組遍歷,轉成對象形式,把type屬性設置null,當傳入的是對象時,又分爲倆種狀況,一種是key值對應的是對象,那直接賦值就好。不然那表明只指定了類型(例如: { sex: String, }),一樣轉成對象形式。

規範化inject

function normalizeInject (options: Object, vm: ?Component) {
  // 取到options.inject的引用
  const inject = options.inject
  if (!inject) return
  // 重置對象,以後從新賦值屬性
  const normalized = options.inject = {}
  // 1. 數組狀況,直接遍歷。與normalizeProps同理
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) { 
    // 2. 對象狀況。若是key值對應的是對象,則經過exntend合併,若是不是,則表明直接是from
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}
複製代碼

其實normalizeInject和normalizeProps函數很類似,都是分爲對象和字符串數組倆種大狀況,對象又分爲倆種小狀況。這裏判斷key值對應的是對象時,多作了一步處理就是用 extend合併對象。由於from屬性不能爲空,因此若是對象中沒有from屬性,默認仍是賦予同名的from。不然就會被覆蓋。例如:如上圖中的age屬性的from值 parentAge就會覆蓋默認的age,而job屬性沒有指定from,因此會賦予同名的from屬性。

規範化directives

function normalizeDirectives (options: Object) {
  const dirs = options.directives
  // 遍歷對象,若是key值對應的是函數。則修改爲對象形式。
  // Vue提供了自定義指令的簡寫,若是隻傳函數,等同於{ bind: func, update: func }
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}
複製代碼

以上三個函數每一個if分支都是根據Vue提供的feature來進行不一樣的處理,其根本目的就是爲了使傳入的參數統一。若是你對哪一個分支還有疑惑,能夠去閱讀下相關的官方文檔。propsinjectdirectiveside


咱們回到mergeOptions函數繼續往下看,這裏判斷沒有_base屬性的話(被合併過再也不處理,只有合併過的選項會帶有_base屬性),處理子選項的extend、mixins,處理方法就是將extend和mixins再經過mergeOptions函數與parent合併,由於mergeOptions函數合併後會返回新的對象,因此這時parent已是個嶄新的對象啦。svg

if (!child._base) {
  // 若是有extends屬性(`extends: xxx`),則仍是調用mergeOptions函數返回的結果賦值給parent
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  // 若是有mixins屬性(`mixins: [xxx, xxx]`)
  // 則遍歷數組,遞歸調用mergeOptions,結果也賦值給parent
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}
複製代碼

接下來的最後一段代碼以下:

// 定義options爲空對象,最後函數返回結果是options
const options = {}
let key
// 先遍歷parent執行mergeField
for (key in parent) {
  mergeField(key)
}
// 再遍歷child,當parent沒有key的時候,在執行mergeField。
// 若是有key屬性,就不須要合併啦,由於上一步已經合併到options上了
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
// 該函數主要是經過key獲取到對應的合併策略函數,而後執行合併,賦值給options[key]
function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
return options
複製代碼

到最後能夠知道,mergeOptions函數進行真正的合併是最後一段代碼,前面都是對選項進行規範化,以及extendmixins進行遞歸合併。那strat是啥呢?其實它是文件頂部定義的一個對象,它是config.optionMergeStrategies的引用,而且在以後對特殊的合併策略進行了重寫,好比說eldata鉤子函數componentspropsmethods等等。合併策略相關的代碼咱們在下一篇進行分析。

參考

相關文章
相關標籤/搜索