上一章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分別是:components
、 directives
、 filters
、 filters
第二個循環的key分別是:el
、 data
這兩個循環都調用了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)
}
}
在非生產環境下添加了el
和propsData
兩個屬性,也就是用來處理el和propsData的選項的合并的,值是一個函數,來看一下這個函數的內容。
先是一個if判斷,如果沒有vm參數,會發出一個警告:提示el
或propsData
選項只能用在用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
選項始終是一個函數。
- 直接創建Vue實例
- 合并子組件選項 —— 沒有父選項有子選項
- 合并子組件選項 —— 沒有子選項有父選項
- 合并子組件選項 —— 父子選項都有
回頭再看一下這兩個函數里面都調用了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
對象上。根據調用函數時的傳參順序可知,to
是childVal
產生的純對象,from
是parentVal
產生的純對象。在看一下函數實現
- 如果
from
不存在,直接返回to
- 循環遍歷
from
的屬性,如果屬性不在to
對象中,則在to
對象上設置對應的屬性和值 - 如果屬性在
to
對象中,并且toVal
和fromVal
都是對象,則遞歸調用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中的資源是components
、 directives
、 filters
。我們再來看一下合并資源的策略函數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
的打印結果:
選項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,然后合并
ret
和parentVal
. 如果子選項存在,再將childVal
的屬性混合到ret
中,這個時候子選項將會覆蓋父選項都同名屬性 - 最后返回ret
選項 provide 的合并策略
strats.provide = mergeDataOrFn
這個很簡單,只有一句代碼,使用函數mergeDataOrFn
, 這個函數我們在data
的策略函數中看到過了。
至此選項的合并已經看完了,之后我們再接著往下看。