Vue的mergeOptions函數分析-下

上篇文章分析了mergeOptions函數的主要邏輯,最後知道是分別遍歷倆個選項對象都去執行mergeField函數,其中mergeField函數實際上是根據不一樣的key值來獲取到相應的合併策略,從而執行真正的合併。接下來咱們主要分析下Vue針對不一樣的內部選項實施的合併策略javascript

defaultStrat

咱們再看一下mergeField函數,當strats[key]不存在時,會採起defaultStrat做爲合併策略。也就是說若是咱們不向Vue.config.optionMergeStrategies添加額外的策略,那就會採起默認的合併策略。html

function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
複製代碼

咱們能夠在當前文件中找到defaultStrat函數以下,默認合併策略超簡單,就是當子選項childVal存在時,就會採用子選項。也就是覆蓋式的合併。vue

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
複製代碼

咱們能夠經過Vue.util.mergeOptions()來看一看defaultStrat的合併效果,以下圖,當咱們父選項parentVal、子選項childVal上都存在name屬性時,合併的對象會採用子選項上的值。可是當咱們設置Vue.config.optionMergeStrategies.name後,mergeField函數就會採用咱們設置好的合併策略,結果會將倆個值相加。 java

默認合併
optionMergeStrategies

el和propsData

if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    // 沒有傳vm說明 不是實例化時候 調用的mergeOptions
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    // 採用默認的策略
    return defaultStrat(parent, child)
  }
}
複製代碼

能夠發現,el和propsData的合併就是採用了默認的合併策略(覆蓋式),但在非生產環境下,會多一步判斷,判斷若是沒有傳vm參數則給出警告,elpropsData參數只能用於實例化。那根據vm就能夠判斷出是不是實例化時候調用的嘛?這裏是確定的。前文咱們提到過Vue.extendVue.mixin調用mergeOptions是不傳入第三個參數的,mergeOptions調用mergeField函數又會把vm傳入進去,因此說vm沒有傳就爲undefined,就能夠說明不是實例化時調用的。再說一點,vm也能夠判斷出是不是處理子組件選項,由於子組件的實現方式是經過實例化子類完成的,而子類又是經過Vue.extend創造出來的。數組

data

strats.data = function ( parentVal: any, childVal: any, vm?: Component): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm)
}
複製代碼

根據?Function能夠知道data的合併會返回一個函數,這裏也會先判斷有沒有傳入vm,若是沒有傳入,會判斷子選項data是不是函數,不是函數的話直接返回父選項data而且給出警告。這個警告應該在咱們剛開始用Vue都有遇到過,這是爲了防止對象引用形成修改會影響到其餘組件的data。若是是函數,則調用mergeDataOrFn函數。那咱們能夠發現,無論傳沒傳vm參數,都會調用mergeDataOrFn函數來返回一個函數。那接下來咱們看一下mergeDataOrFn函數幹了什麼。瀏覽器

export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component): ?Function {
  // 沒有vm參數,表明是用 Vue.extend、Vue.mixin合併,
  if (!vm) {
    // 若是沒有childVal,返回parentVal 
    if (!childVal) {
      return parentVal
    }
    // 若是沒有parentVal,返回childVal
    if (!parentVal) {
      return childVal
    }
    // 返回一個合併data函數
    return function mergedDataFn () {
       // 當調用mergedDataFn纔會執行mergeData
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    // 返回一個合併data函數
    return function mergedInstanceDataFn () {
      // 實例化合並,判斷是不是函數,函數執行獲得對象。
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        // 若是子選項data有值,則經過mergeData合併。
        // 當調用mergedInstanceDataFn纔會執行mergeData
        return mergeData(instanceData, defaultData)
      } else {
        // 子選項data沒有值,直接返回默認data
        return defaultData
      }
    }
  }
}
複製代碼

上面mergeDataOrFn函數分爲倆種狀況,一種是沒有vm參數說明是處理子組件合併data選項的,另外一種是有vm參數說明是實例化處理data的合併。ide

咱們先看一下處理子組件的狀況,首先判斷沒有childVal返回parentVal,沒有parentVal返回childVal。若是倆個都有值,則返回mergedDataFn函數。下圖分別展現了三種狀況的合併效果。函數

  • 經過Parent.mixin()觸發合併,此時parentVal已經通過Vue.extend合併過。childVal沒有data選項

parentVal有、childVal沒有

  • 經過Vue.extend()觸發合併,此時parentValVue.options沒有data選項,childValdata選項

parentVal沒有、childVal有

  • parentValchildVal都有值時,返回mergedDataFn函數

parentVal和childVal都有

再看一下處理實例化時data合併的狀況,處理實例化時data合併直接返回mergedInstanceDataFn函數。 post

實例化合並data

咱們能夠發現,倆種狀況都是返回函數,而且函數中都是先判斷parentValchildVal是不是函數,是函數的話直接執行函數獲取純對象,最後都經過mergeData來返回合併後的純對象。因此說mergeData函數是真正用來合併選項對象的,那咱們在來看一下這個函數。fetch

// 將from的屬性添加到to上,最後返回to
function mergeData (to: Object, from: ?Object): Object {
  // 若是沒有from、直接返回to
  if (!from) return to
  let key, toVal, fromVal
  // 取到from的key值,用於遍歷
  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)
  
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // 對象被觀察了,會有__ob__屬性,__ob__不做處理
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    // 若是to上沒有該屬性,則直接將from對應的值賦值給to[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // 若是 to、from都有值,而且不相同,並且都是純對象的話,
      // 則遞歸調用mergeData進行合併
      mergeData(toVal, fromVal)
    }
  }
  return to
}
複製代碼

mergeData函數很簡單,就是將parentVal的data純對象(from)所擁有的屬性添加到childVal的data純對象(to),最後返回合併的純對象。若是其中倆個純對象上有相同的key值,則比較是否相等,若是相等什麼都不用作,不相等的話,則遞歸合併。

Hook鉤子函數

// 鉤子函數當作數組合並來處理,最後返回數組
function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> {
  const res = childVal
    ? parentVal  // childVal有值
      ? parentVal.concat(childVal) // parentVal有值,與childVal直接數組拼接
      : Array.isArray(childVal) // parentVal沒有值,將childVal變成數組
        ? childVal
        : [childVal]
    // childVal沒有值直接返回parentVal
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}
// 去重操做
function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}
// src/shared/constants 文件夾定義
const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
// 全部鉤子函數採用一種合併策略
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})
複製代碼

生命週期的鉤子選項合併也很是簡單,就是把合併當作數組拼接。若是其中一個純對象沒有,則把另外一方變成數組返回。這裏有倆點須要提一下:

  1. 判斷childVal沒有直接返回parentVal當倆個對象都有的時候經過parentVal.concat()拼接,都直接把parentVal當作數組來處理。說明parentVal必定是數組,由於若是parentVal有值,那必定是被mergeOptions處理過一次啦,因此會變成數組。

  2. 上面有判斷Array.isArray(childVal),而不是直接變成數組。,說明childVal能夠是個數組,以下圖,咱們能夠給created傳入數組也能夠

    傳入數組

component、directive、filter

function mergeAssets ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): Object {
  // 建立一個空對象,經過res.__proto__能夠訪問到parentVal
  const res = Object.create(parentVal || null)
  // 若是childVal有值,則校驗childVal[key]是不是對象,不是給出警告。
  // extend函數是將childVal的屬性添加到res上,
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
// component、directive、filter
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})
複製代碼

componentdirectivefilter選項的合併是先將parentVal添加到res.__proto__上,而後把childVal添加到res上。當咱們使用組件時,Vue會一層一層的向上查找。這也就是爲何咱們沒有引入KeepAliveTransitionTransitionGroup內置組件,卻能夠直接在template中使用,由於在合併時,就已經將內置組件合併到components對象的原型鏈上。以下圖:

原型鏈component

watch

strats.watch = function ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
  // Firefox瀏覽器自帶watch,若是是原生watch,則置空
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  // 若是沒有childVal,則建立返回空對象,經過__proto__能夠訪問parentVal
  if (!childVal) return Object.create(parentVal || null)
  // 非正式環境檢驗校驗childVal[key]是不是對象,不是給出警告。
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // 若是沒有parentVal,返回childVal
  if (!parentVal) return childVal
  // parentVal和childVal都有值的狀況
  const ret = {}
  // 把parentVal屬性添加到ret
  extend(ret, parentVal)
  // 遍歷childVal
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    // 若是parent存在,則變成數組
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    // 返回數組
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}
複製代碼

watch的選項合併簡單說就是判斷父子是否都有監聽同一個值,若是同時監聽了,就變成一個數組。不然就正常合併到一個純對象上就能夠。watch也能夠爲一個值建立監聽數組,例如:

export default {
  watch: {
    key: [
      function() {
        console.log('key 改變1')
      },
      function() {
        console.log('key 改變2')
      }
    ]
  }
}
複製代碼

props、methods、inject、computed

strats.props =
strats.methods =
strats.inject =
strats.computed = function ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
  // 非正式環境檢驗校驗childVal[key]是不是對象,不是給出警告。
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // 若是沒有parentVal 返回childVal
  if (!parentVal) return childVal
  const ret = Object.create(null)
  // 將parentVal屬性添加到ret
  extend(ret, parentVal)
  // 若是childVal有值,也將屬性添加到ret
  if (childVal) extend(ret, childVal)
  return ret
}
複製代碼

propsmethodsinjectcomputed選項的合併是合併到同一個純對象上,對於父子有一樣的key值,採起子選型上對應的值。

provide

// provide選型合併採用data選項的合併策略
strats.provide = mergeDataOrFn
複製代碼

參考

相關文章
相關標籤/搜索