1. 程式人生 > >vue源碼學習--合並策略對象mergeOptions

vue源碼學習--合並策略對象mergeOptions

undefine amp err ams extends remount span 選擇 特點

源碼vue在實例化對象、vue子類聲明的時候會對父實例和子實例的參數使用設定好的合並策略合並父、子實例的參數。以及實例化前期、數據綁定時均有使用到合並策略合並參數。

定義合並策略的js文件路徑是:\vue-dev\src\core\util\options.js

在合並策略中對不同類型的參數使用了不同的合並策略。例如:strat.data合並data、defaultStrat合並[el、propsData和name]、mergrHook 合並生命周期的鉤子函數、mergeAssets合並[component、directives、filter]等。

這些合並策略通過入口函數mergeOptions (parent, child, vm)中對合並參數對象中的不同屬性進行合並策略選擇。

技術分享

 1 export function mergeOptions (
 2   parent: Object,
 3   child: Object,
 4   vm?: Component
 5 ): Object {
 6   if (process.env.NODE_ENV !== ‘production‘) {
 7     checkComponents(child)
 8   }
 9 
10   if (typeof child === ‘function‘) {
11     child = child.options
12   }
13 
14   normalizeProps(child, vm)//
格式化prop為基於對象的格式 15 normalizeInject(child, vm)//格式化Inject為基於對象的格式 16 normalizeDirectives(child)//格式化directives為對象的格式 17 const extendsFrom = child.extends 18 if (extendsFrom) { 19 parent = mergeOptions(parent, extendsFrom, vm) 20 } 21 if (child.mixins) { 22 for (let i = 0, l = child.mixins.length; i < l; i++) {
23 parent = mergeOptions(parent, child.mixins[i], vm) 24 } 25 } 26 const options = {} 27 let key 28 for (key in parent) { 29 mergeField(key) 30 } 31 for (key in child) { 32 if (!hasOwn(parent, key)) { 33 mergeField(key) 34 } 35 } 36 function mergeField (key) { 37 const strat = strats[key] || defaultStrat 38 options[key] = strat(parent[key], child[key], vm, key) 39 } 40 return options 41 }

從上面mergeField函數中可以看出,Strats綁定處理參數中的各種數據的方法,統一在入口方法mergeOptions中被調用。源碼在定義strats的時的註釋也做了相應的說明,如下:

1 /**
2  * Option overwriting strategies are functions that handle
3  * how to merge a parent option value and a child option
4  * value into the final value.
5  */
6 const strats = config.optionMergeStrategies
  1. 合並生命周期的鉤子函數和props參數的方法為mergeHook
 1 export const LIFECYCLE_HOOKS = [
 2   ‘beforeCreate‘,
 3   ‘created‘,
 4   ‘beforeMount‘,
 5   ‘mounted‘,
 6   ‘beforeUpdate‘,
 7   ‘updated‘,
 8   ‘beforeDestroy‘,
 9   ‘destroyed‘,
10   ‘activated‘,
11   ‘deactivated‘,
12   ‘errorCaptured‘
13 ]
14 LIFECYCLE_HOOKS.forEach(hook => { 15 strats[hook] = mergeHook 16 })

mergeHook方法實現思路及源碼如下:

技術分享

用人話總結這個合並規則就是:只有父時返回父,只有子時返回數組類型的子。父、子都存在時,將子添加在父的後面返回組合而成的數組。這也是父子均有鉤子函數的時候,先執行父的後執行子的的原因。源碼如下:

 1 /**
 2  * Hooks and props are merged as arrays.
 3  */
 4 function mergeHook (
 5   parentVal: ?Array<Function>,
 6   childVal: ?Function | ?Array<Function>
 7 ): ?Array<Function> {
 8   return childVal
 9     ? parentVal
10       ? parentVal.concat(childVal)
11       : Array.isArray(childVal)
12         ? childVal
13         : [childVal]
14     : parentVal
15 }

2.strats.data合並data數據,代碼邏輯如下:

技術分享

源碼如下:

 1 strats.data = function (
 2   parentVal: any,
 3   childVal: any,
 4   vm?: Component
 5 ): ?Function {
 6   if (!vm) {
 7     if (childVal && typeof childVal !== ‘function‘) {
 8       process.env.NODE_ENV !== ‘production‘ && warn(
 9         ‘The "data" option should be a function ‘ +
10         ‘that returns a per-instance value in component ‘ +
11         ‘definitions.‘,
12         vm
13       )
14 
15       return parentVal
16     }
17     return mergeDataOrFn.call(this, parentVal, childVal)
18   }
19 
20   return mergeDataOrFn(parentVal, childVal, vm)
21 }

由源碼最後可知無論是vm存在與否最後都調用了mergeDataOrFn函數。這個函數根據vm是否存在,對parentVal和childVal做出不同的處理,但是無論vm存在不存在最終都會調用mergeData函數,將parentVal和childVal合並成最終值。所以介紹mergeDataOrFn函數之前先介紹mergeData這個函數。源碼如下:

 1 function mergeData (to: Object, from: ?Object): Object {
 2   if (!from) return to
 3   let key, toVal, fromVal
 4   const keys = Object.keys(from)
 5   for (let i = 0; i < keys.length; i++) {
 6     key = keys[i]
 7     toVal = to[key]
 8     fromVal = from[key]
 9     if (!hasOwn(to, key)) {
10       set(to, key, fromVal)
11     } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
12       mergeData(toVal, fromVal)
13     }
14   }
15   return to
16 }

用人話總結這個合並規則就是:

1.如果from【childVal】中的某個屬性to【parentVal】中也有,保留to中的,什麽也不做

2.如果to中沒有,將這個屬性添加到to中

3.如果to和from中的某個屬性值都是對象,則遞歸調用,進行深度合並。

無論vm存在不存在mergeDataOrFn最終都會調用mergeData函數,將parentVal和childVal合並成最終值。那麽接下來看mergeDataOrFn中對parentVal和childVal做了什麽處理。

邏輯圖如下:

技術分享

源碼如下:

 1 export function mergeDataOrFn (
 2   parentVal: any,
 3   childVal: any,
 4   vm?: Component
 5 ): ?Function {
 6   if (!vm) {
 7     // in a Vue.extend merge, both should be functions
 8     if (!childVal) {
 9       return parentVal
10     }
11     if (!parentVal) {
12       return childVal
13     }
14     // when parentVal & childVal are both present,
15     // we need to return a function that returns the
16     // merged result of both functions... no need to
17     // check if parentVal is a function here because
18     // it has to be a function to pass previous merges.
19     return function mergedDataFn () {
20       return mergeData(
21         typeof childVal === ‘function‘ ? childVal.call(this) : childVal,
22         typeof parentVal === ‘function‘ ? parentVal.call(this) : parentVal
23       )
24     }
25   } else if (parentVal || childVal) {
26     return function mergedInstanceDataFn () {
27       // instance merge
28       const instanceData = typeof childVal === ‘function‘
29         ? childVal.call(vm)
30         : childVal
31       const defaultData = typeof parentVal === ‘function‘
32         ? parentVal.call(vm)
33         : parentVal
34       if (instanceData) {
35         return mergeData(instanceData, defaultData)
36       } else {
37         return defaultData
38       }
39     }
40   }
41 }

3.strats.provide = mergeDataOrFn

4.strats.watch源碼如下:

 1 /**
 2  * Watchers.
 3  *
 4  * Watchers hashes should not overwrite one
 5  * another, so we merge them as arrays.
 6  */
 7 strats.watch = function (
 8   parentVal: ?Object,
 9   childVal: ?Object,
10   vm?: Component,
11   key: string
12 ): ?Object {
13   // work around Firefox‘s Object.prototype.watch...
14   if (parentVal === nativeWatch) parentVal = undefined
15   if (childVal === nativeWatch) childVal = undefined
16   /* istanbul ignore if */
17   if (!childVal) return Object.create(parentVal || null)
18   if (process.env.NODE_ENV !== ‘production‘) {
19     assertObjectType(key, childVal, vm)
20   }
21   if (!parentVal) return childVal
22   const ret = {}
23   extend(ret, parentVal)
24   for (const key in childVal) {
25     let parent = ret[key]
26     const child = childVal[key]
27     if (parent && !Array.isArray(parent)) {
28       parent = [parent]
29     }
30     ret[key] = parent
31       ? parent.concat(child)
32       : Array.isArray(child) ? child : [child]
33   }
34   return ret
35 }

註釋裏說了:watchers不應該重寫,應該保存在一個數組裏。這就是watch數據合並的策略核心。

1.定義ret並讓ret活得parentVal的所有屬性。

2.遍歷 childVal的所有屬性,如果ret(即parentVal)中也有的話,就把ret的屬性值弄成一個數組,把childVal的同名屬性值放在ret同名值得後面。如果不存在就把childVal弄成一個數組。

3.最後都將數組的值賦值給ret,拓展ret的屬性和屬性值。

這個策略其實就是,子組件、父組件都存在的時候,把watch相同值得方法放在一個數組裏,父前子後。

5.strats.component、strats.directive、strats.filter源碼如下:

 1 export const ASSET_TYPES = [
 2   ‘component‘,
 3   ‘directive‘,
 4   ‘filter‘
 5 ]
 6 /**
 7  * Assets
 8  *
 9  * When a vm is present (instance creation), we need to do
10  * a three-way merge between constructor options, instance
11  * options and parent options.
12  */
13 function mergeAssets (
14   parentVal: ?Object,
15   childVal: ?Object,
16   vm?: Component,
17   key: string
18 ): Object {
19   const res = Object.create(parentVal || null)
20   if (childVal) {
21     process.env.NODE_ENV !== ‘production‘ && assertObjectType(key, childVal, vm)
22     return extend(res, childVal)
23   } else {
24     return res
25   }
26 }
27 
28 ASSET_TYPES.forEach(function (type) {
29   strats[type + ‘s‘] = mergeAssets
30 })

這個合並策略的核心就是:將childVal的全部屬性通過原型委托在parentVal上。parentVal成為了childVal的原型對象。

所以需要查找某個component、directive、filter,首先會在childVal中查找,如果沒有就在其原型對象上查找。

即子組件有就用子組件的,子組件沒有向上在父組件中尋找。

6.strats.props 、strats.methods 、strats.inject 、strats.computed源碼如下:

 1 /**
 2  * Other object hashes.
 3  */
 4 strats.props =
 5 strats.methods =
 6 strats.inject =
 7 strats.computed = function (
 8   parentVal: ?Object,
 9   childVal: ?Object,
10   vm?: Component,
11   key: string
12 ): ?Object {
13   if (childVal && process.env.NODE_ENV !== ‘production‘) {
14     assertObjectType(key, childVal, vm)
15   }
16   if (!parentVal) return childVal
17   const ret = Object.create(null)
18   extend(ret, parentVal)
19   if (childVal) extend(ret, childVal)
20   return ret
21 }

這種合並策略的特點就是子會覆蓋父。

1.先將parentVal的所有屬性擴展給res

2.再將childVal的所有屬性擴展給res。此時,若是parentVal和childVal擁有同名屬性的話,子的屬性就會覆蓋父的。也就是同名方法只會執行子的。

7.其他的屬性使用的就是默認合並策略:defaultStrat。源碼如下:

1 /**
2  * Default strategy.
3  */
4 const defaultStrat = function (parentVal: any, childVal: any): any {
5   return childVal === undefined
6     ? parentVal
7     : childVal
8 }

默認策略就是:子組件的選項不存在,才會使用父組件的選項,如果子組件的選項存在,使用子組件自身的。

因為是不會對parentVal和childVal進行分解的。所以默認策略一半用於合並比較簡單,不包含函數的屬性,例如el。

1 const defaultStrat = function (parentVal: any, childVal: any): any {
2 return childVal === undefined
3 ? parentVal
4 : childVal
5 }

註:本文章中學習的源碼版本為vue 2.5.2. 文章中涉及的觀點和理解均是個人的理解,如有偏頗或是錯誤還請大神指出,不吝賜教,萬分感謝~

vue源碼學習--合並策略對象mergeOptions