Vue源碼解析三——選項合并

上一章Vue源碼解析二——從一個小例子開始逐步分析看完規范化選項之后,再來看看合并階段是如何處理的,接下來是mergeOptions函數剩下的代碼:

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函數最后是返回了一個新的對象,中間的代碼就是充實這個新對象的。

兩個for in循環分別遍歷了parent和child,并以對象的鍵為參數調用了mergeField函數。在遍歷child的循環中多了一個判斷,如果在parent中出現過的鍵就不再進行處理了。

在我們之前所舉的例子中,parent就是Vue.options, child就是我們實例化時傳過來的參數。

// parent
Vue.options = {
  components: {
      KeepAlive,
      Transition,
      TransitionGroup
  },
  directives: {
      model,
      show
  },
  filters: Object.create(null),
  _base: Vue
}

// child
{
  el: '#app',
  data: { a: 1 }
}

所以第一個循環的key分別是:componentsdirectivesfiltersfilters

第二個循環的key分別是:eldata

這兩個循環都調用了mergeField函數,所以真正的實現在該函數中:

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

該函數先是定義了strat常量,它的值是是根據參數key得到的strats里面的值,沒有的話就是defaultStrat. 然后以parent[key], child[key], vm, key為參數調用了strat,并把返回值賦給了options[key]. 所以我們還是要先弄清楚strats是什么。這就要從core/util/options.js文件的開頭看起了。

/**
 * 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

這句代碼就定義了strats常量,它的值是config.optionMergeStrategies, 來自于core/config.js文件。現在它的值還是一個空對象。我們翻譯一下上面的注釋:

選項覆蓋策略是處理如何將父選項值和子選項值合并到最終值的函數。
所以config.optionMergeStrategies是一個包含策略函數的對象,接下來就是要根據不同的選項定義不同的策略函數了。

選項 el、propsData 的合并策略

接著往下看代碼:

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)
  }
}

在非生產環境下添加了elpropsData兩個屬性,也就是用來處理el和propsData的選項的合并的,值是一個函數,來看一下這個函數的內容。

先是一個if判斷,如果沒有vm參數,會發出一個警告:提示elpropsData選項只能用在用new創建實例的時候。

否則直接調用defaultStrat函數并返回,該函數的定義也在options.js文件中:

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

根據注釋可知,它是默認的策略函數。函數體也很簡單,就是判斷childVal是否存在,如果存在就返回childVal,否則返回parentVal。

選項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)
}

該策略函數用來合并處理data選項。首先也是一個if語句判斷是否傳遞了vm參數,那么這個vm參數代表什么呢?我們是在mergeOptions函數中看到mergeField里面有調用才來到這里看策略函數的:

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

傳給strat的vm參數是mergeOptions接收的參數,這個參數從何而來呢?是我們在_init函數中調用mergeOptions時傳進來的

const vm: Component = this
vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

所以vm就是Vue實例。

既然判斷了vm是否存在,那什么時候沒有傳遞vm參數呢?在Vue.extend方法中也有調用mergeOptions函數,這里就沒有傳遞vm參數:

Sub.options = mergeOptions(
    Super.options,
    extendOptions
)

我們知道子組件是通過實例化Vue.extend創造的子類實現的,也就是說子組件的實例化不會傳遞vm參數。

接著看if里面的內容:

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)

如果存在childVal(即data選項)并且它不是一個函數,就會在非生產環境下發出警告:data選項必須是一個函數,并且直接返回parentVal。也就是說子組件的data選項必須是一個函數

否則調用mergeDataOrFn函數并返回其執行結果。

如果vm存在,也是返回mergeDataOrFn的執行結果,但是會多傳一個vm參數。

既然如論是子組件的data選項還是非子組件的選項都調用了mergeDataOrFn函數,我們就來看看該函數吧

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
  } else {
    ...
}

代碼整體結構就是if else判斷,if里面是處理子組件選項的,我們先看一下:

// in a Vue.extend merge, both should be functions
if (!childVal) {
    return parentVal
}
if (!parentVal) {
    return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
    return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
    )
}

最上面的一句注釋是說在Vue.extend合并處理中,父子data選項都應該是一個函數

下面是兩個if判斷:如果子選項不存在就返回父選項,如果父選項不存在就返回子選項。

如果都存在繼續執行,最后返回一個mergedDataFn函數,里面是mergeData的返回結果,但是它還沒有執行,因為mergedDataFn函數還沒有執行。

接下來再看else語句,也就是處理非子組件的內容:

return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
}

直接返回了mergedInstanceDataFn函數。也就是說不管是不是子組件,mergeDataOrFn都返回一個函數,即data選項始終是一個函數。

  1. 直接創建Vue實例
mergeOptions1.png
  1. 合并子組件選項 —— 沒有父選項有子選項
mergeOptions2.png
  1. 合并子組件選項 —— 沒有子選項有父選項
mergeOptions3.png
  1. 合并子組件選項 —— 父子選項都有
mergeOptions4.png

回頭再看一下這兩個函數里面都調用了mergeData函數,返回的也是它的調用結果,所以真正合并data選項的處理在該函數中。在看函數實現之前,先看看該函數接收的參數。

無論是mergedDataFn還是mergedInstanceDataFn函數都有如下對父子選項的處理:

typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal

mergeData函數接收的參數正是這個處理之后的結果,也就是兩個純對象。接下來再看函數實現:

/**
 * Helper that recursively merges two data objects together.(已遞歸的方式將兩個對象合并在一起)
 */
function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

根據函數上方的注釋我們知道該函數的功能是將兩個對象合并到一起,函數最后返回to,可知是將from的屬性混合到了to對象上。根據調用函數時的傳參順序可知,tochildVal產生的純對象,fromparentVal產生的純對象。在看一下函數實現

  • 如果from不存在,直接返回to
  • 循環遍歷from的屬性,如果屬性不在to對象中,則在to對象上設置對應的屬性和值
  • 如果屬性在to對象中,并且toValfromVal都是對象,則遞歸調用mergeData對屬性值進行深度合并

到這里,data選項的策略函數我們就看完了

生命周期鉤子選項的策略合并

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

LIFECYCLE_HOOKS定義在shared/constants.js文件中:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

正是我們所熟知的生命周期鉤子。循環遍歷了LIFECYCLE_HOOKS數組,以遍歷hook為鍵mergeHook函數為值添加到了strats策略對象中。所有的鉤子函數都是相同的策略函數mergeHook,我們來看一下該函數:

/**
 * Hooks and props are merged as arrays.
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

根據函數上方的注釋可知最后被合并成了數組。函數體中res的結果由三組三木運算所得

  • 如果子選項不存在直接等于父選項的值
  • 如果子選項存在,再判斷父選項。如果父選項存在, 就把父子選項合并成一個數組
  • 如果父選項不存在,判斷子選項是不是一個數組,如果不是數組將其作為數組的元素

如果子選項不存在直接返回父選項的值,父選項一定是數組嗎?我們來看這樣一個例子:

var Pub = Vue.extend({
  beforeCreate: function () {
    console.log('pub')
  }
})

var Sub = Pub.extend({})

輸出Sub.options.beforeCreate的值
[{
  beforeCreate: function () {
    console.log('pub')
  }
}]

調用Vue.extend時在mergeOptions執行鉤子函數的合并策略時,parentVal是Vue.options.beforeCreate是undefined,childVal是beforeCreate: function () { console.log('pub') }, childVal存在parentVal不存在執行這個:

Array.isArray(childVal)
        ? childVal
        : [childVal]

所以最后Pub.options.beforeCreate的值是:

Pub.options.beforeCreate = [{
  beforeCreate: function () {
    console.log('pub')
  }
}]

而Pub.extend({})執行合并策略時,parentVal的值是

[{
  beforeCreate: function () {
    console.log('pub')
  }
}]

childVal的值是undefined,直接返回父選項,的確結果是一個數組。

當parentVal有值時執行

parentVal.concat(childVal)

根據前面的分析我們知道parentVal是一個數組,所以可以調用concat方法。

mergeHook函數的最后是這樣一段代碼:

return res
    ? dedupeHooks(res)
    : res

res有值則以它為參數調用dedupeHooks函數,返回值作為最終結果返回,否則直接返回res. 我們看一下dedupeHooks函數:

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
}

這個函數的作用主要是把重復數據刪除(不過感覺沒用啊。。)

資源(assets)選項的合并策略

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

都有哪些資源? 我們看一下ASSET_TYPES的值是什么。該常量定義在shared/constants.js文件中:

export const ASSET_TYPES = [
  'component',
  'directive', 
  'filter'
]

在循環中又給每個元素后面加了字符s,所以Vue中的資源是componentsdirectivesfilters。我們再來看一下合并資源的策略函數mergeAssets:

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

這段代碼邏輯很簡單,首先是以parentVal為原型創建對象 res, 然后判斷是否存在childVal, 如果有的話調用extend()childVal合并到res上,沒有就直接返回res。

我們可以在任意的組件中使用像KeepAlive這種內置組件,就是因為這句

Object.create(parentVal || null)

在合并資源的時候把Vue內置組件放到了原型上,這樣即使子組件中沒有注冊這樣的組件,也會尋著原型鏈查找,找到了就可以使用。比如:

var vm = new Vue({
    components: {
        testA: { data() {}}
    }
})

我們看一下vm.$options.components的打印結果:

components.png

選項watch的合并策略

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

先看看這兩句:

// work around Firefox's Object.prototype.watch...(解決Firefox的Object.prototype.watch)
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined

在Firefox瀏覽器中Object.prototype擁有原生的watch屬性,所以即使我們實例化時沒有傳遞watch屬性,在FireFox中也能獲取到watch屬性,這就會在處理時造成困擾。所以這里的判斷是如果選項是原生watch,就把選項置為undefined,就不用處理了。nativeWatch定義在core/util/env.js 文件中:

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch

在 Firefox 中時 nativeWatch 為原生提供的函數,在其他瀏覽器中 nativeWatch 為 undefined.

接下來是三個if判斷:

if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
  • 如果不存在子選項,直接返回以父選項為原型創建的對象
  • 子選項存在,在非生產環境下判斷子選項是否是純對象
  • 如果父選項不存在,直接返回子選項

接下來就是父子選項都存在的處理:

const ret = {} // 創建空對象
extend(ret, parentVal) // 把父選項合并到空對象中
for (const key in childVal) { // 循環子選項
    let parent = ret[key] // 取父選項中當前屬性的值,不一定存在
    const child = childVal[key] // 子選項中當前屬性的值
    if (parent && !Array.isArray(parent)) {
      parent = [parent] // 如果父選項存在且不是數組,以值為元素包裹成數組
    }
    ret[key] = parent
      ? parent.concat(child) // 如果parent存在,直接合并父選項和子選項中當前屬性的值
      : Array.isArray(child) ? child : [child] // 不存在,將child轉成數組返回
}
return ret

總之就是把子選項的每一項變成數組返回了。

所以watch選項下的每一項,有可能是函數(當父選項不存在時),可能是數組(父子選項都存在)

選項 props、methods、inject、computed 的合并策略

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}
  • 首先是當子選項存在并且在非生產環境下時,檢測子選項是不是純對象
  • 如果父選項不存在,直接返回父選項
  • 先是以null為原型創建對象ret,然后合并retparentVal. 如果子選項存在,再將childVal的屬性混合到ret中,這個時候子選項將會覆蓋父選項都同名屬性
  • 最后返回ret

選項 provide 的合并策略

strats.provide = mergeDataOrFn

這個很簡單,只有一句代碼,使用函數mergeDataOrFn, 這個函數我們在data的策略函數中看到過了。

至此選項的合并已經看完了,之后我們再接著往下看。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容,還有我對于 Vue 1.0 印象不深的內容。關于...
    云之外閱讀 5,072評論 0 29
  • 回憶 首先,render函數中手寫h=>h(app),new Vue()實例初始化init()和原來一樣。$mou...
    LoveBugs_King閱讀 2,296評論 1 2
  • Vue 實例 屬性和方法 每個 Vue 實例都會代理其 data 對象里所有的屬性:var data = { a:...
    云之外閱讀 2,233評論 0 6
  • vue概述 在官方文檔中,有一句話對Vue的定位說的很明確:Vue.js 的核心是一個允許采用簡潔的模板語法來聲明...
    li4065閱讀 7,266評論 0 25
  • ORA-00001: 違反唯一約束條件 (.) 錯誤說明:當在唯一索引所對應的列上鍵入重復值時,會觸發此異常。 O...
    我想起個好名字閱讀 5,407評論 0 9