上一節深入剖析Vue原始碼 - 選項合併(上)的末尾,我們介紹了Vue中處理合並選項的思路,概括起來主要有兩點,一是當選項存在定義好的預設配置策略時,優先選擇預設配置策略,並且根據不同的配置項來合併子父選項; 二是當傳入選項不存在預設策略時,處理的原則是有子類配置選項則預設使用子類配置選項,沒有則選擇父類配置選項。vue中,大部分選項都有其自定義策略,因此本節分析的重點也放在了各種自定義配置策略中(內建資源選項,生命週期鉤子選項,el, data, watch, props等)。
首先還是回顧一下選項合併的程式碼,strat這個物件包含了所以自定義的預設策略。
function mergeOptions ( parent, child, vm ) {
···
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
var strat = strats[key] || defaultStrat; // 如果有自定義選項策略,則使用自定義選項策略,否則選擇子類配置選項
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
複製程式碼
1.5 資源選項的自定義策略
在上一節中,我們知道Vue建構函式自身有options的配置選項,分別是components元件, directive指令, filter過濾器,在建立例項之前,程式會將內建元件和內建指令分別掛載到components和directive屬性上。
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
ASSET_TYPES.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null); // Vue構造器擁有的預設資源選項配置
});
// Vue內建元件
var builtInComponents = {
KeepAlive: KeepAlive
};
var platformComponents = {
Transition: Transition,
TransitionGroup: TransitionGroup
};
// Vue 內建指令,例如: v-model, v-show
var platformDirectives = {
model: directive,
show: show
};
// 將_from物件合併到to物件,屬性相同時,則覆蓋to物件的屬性
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
extend(Vue.options.components, builtInComponents);
extend(Vue.options.components, platformComponents); // 擴充套件內建元件
extend(Vue.options.directives, platformDirectives); // 擴充套件內建指令
複製程式碼
建構函式的預設資源選項配置如下:
Vue.options = {
components: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
},
directives: {
model: {inserted: ƒ, componentUpdated: ƒ}
show: {bind: ƒ, update: ƒ, unbind: ƒ}
},
filters: {}
_base
}
複製程式碼
在例項化Vue,或者例項化子類時,這一類資源選項是如何合併的呢?
// 資源選項自定義合併策略
function mergeAssets (parentVal,childVal,vm,key) {
var res = Object.create(parentVal || null); // 建立一個空物件,其原型指向父類的資源選項。
if (childVal) {
assertObjectType(key, childVal, vm); // components,filters,directives選項必須為物件
return extend(res, childVal) // 子類選項賦值給空物件
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets; // 定義預設策略
});
複製程式碼
簡單總結一下,對於 directives、filters 以及 components 等資源選項,父類選項將以原型鏈的形式被處理。子類必須通過原型鏈才能查詢並使用內建元件和內建指令。
1.6 生命週期鉤子選項自定義策略
我們知道掌握vue的生命週期鉤子是使用vue高效開發元件的重點,這是vue官方的生命週期圖
從原始碼中我們也可以看到vue中有多達12個鉤子,而在選項合併的時候,生命週期鉤子選項是遵循的以下的規則合併的。
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook; // 對生命週期鉤子選項的合併都執行mergeHook策略
});
function mergeHook (parentVal,childVal) {
var res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal; // 1.如果子類和父類都擁有鉤子選項,則將子類選項和父類選項合併, 2如果父類不存在鉤子選項,子類存在時,則以陣列形式返回子類鉤子選項, 3.當子類不存在鉤子選項時,則以父類選項返回。
return res
? dedupeHooks(res)
: res
}
// 防止多個元件例項鉤子選項相互影響
function dedupeHooks (hooks) {
var res = [];
for (var i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res
}
複製程式碼
簡單總結,對於生命週期鉤子選項,子類和父類的選項將合併成陣列,這樣每次執行子類的鉤子函式時,父類鉤子選項也會執行。
1.7 其他自定義策略
Vue自定義選項策略還有很多,我們繼續列舉其他幾個例子。
1.7.1 el合併
我們只在建立vue的例項時才會執行節點掛載,在子類或者子元件中無法定義el選項,程式碼實現如下
strats.el = function (parent, child, vm, key) {
if (!vm) { // 只允許vue例項才擁有el屬性,其他子類構造器不允許有el屬性
warn(
"option \"" + key + "\" can only be used during instance " +
'creation with the `new` keyword.'
);
}
return defaultStrat(parent, child)
};
// 使用者自定義選項策略
var defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
};
複製程式碼
1.7.2 data合併
另一個合併的重點是data的合併策略,data在vue建立例項時傳遞的是一個物件,而在元件內部定義時只能傳遞一個函式,
strats.data = function (parentVal, childVal, vm) {
if (!vm) {
if (!vm) {// 判斷是否為Vue建立的例項,否則為子父類的關係
if (childVal && typeof childVal !== 'function') { // 必須保證子類的data型別是一個函式而不是一個物件
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); // vue例項時需要傳遞vm作為函式的第三個引數
};
複製程式碼
做了data選項的檢驗後,重點關注mergeDataOrFn函式的內部邏輯,程式碼中依然通過vm來區分是否為子類構造器的data合併。
function mergeDataOrFn ( parentVal, childVal, vm ) {
if (!vm) {
if (!childVal) { // 子類不存在data選項,則合併結果為父類data選項
return parentVal
}
if (!parentVal) { // 父類不存在data選項,則合併結果為子類data選項
return childVal
}
return function mergedDataFn () { // data選項在父類和子類同時存在的情況下返回的是一個函式
// 子類例項和父類例項,分別將子類和父類例項中data函式執行後返回的物件傳遞給mergeData函式做資料合併
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// vue建構函式例項物件
return function mergedInstanceDataFn () {
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
// 當例項中傳遞data選項時,將例項的data物件和Vm建構函式上的data屬性選項合併
return mergeData(instanceData, defaultData)
} else {
// 當例項中不傳遞data時,預設返回Vm建構函式上的data屬性選項
return defaultData
}
}
}
}
複製程式碼
如何實現資料合併,資料合併時,vue會將資料變化加入響應式系統中,我們先跳過響應式系統的構建部分,只關注單純的資料合併。資料合併的原則是,將父類的資料整合到子類的資料選項中, 如若父類資料和子類資料衝突時,保留子類資料。
function mergeData (to, from) {
if (!from) { return to }
var key, toVal, fromVal;
var keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
if (!hasOwn(to, key)) {
set(to, key, fromVal); // 當子類資料選項不存在父類的選項時,將父類資料合併到子類資料中,並加入響應式系統中。
} else if ( // 處理深層物件,當合並的資料為多層巢狀物件時,需要遞迴呼叫mergeData進行比較合併
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal);
}
}
return to
}
複製程式碼
思考一個問題,為什麼Vue元件的data是一個函式,而不是一個物件呢? 我覺得這樣可以方便理解:元件的目的是為了複用,每次通過函式建立相當於在一個獨立的記憶體空間中生成一個data的副本,這樣每個元件之間的資料不會互相影響。
1.7.3 watch 選項合併
對於 watch 選項的合併處理,類似於生命週期鉤子,只要父選項有相同的觀測欄位,則合併為陣列,在選項改變時同時執行父類選項的監聽程式碼。處理方式和生命鉤子選項的區別在於,生命鉤子選項必須是函式或者資料,而watch選項則為物件。
strats.watch = function (parentVal,childVal,vm,key) {
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
if (!childVal) { return Object.create(parentVal || null) }
{
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
複製程式碼
1.7.4 props,methods, inject, computed 合併
// 其他選項合併策略
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal,childVal,vm,key) {
if (childVal && "development" !== 'production') {
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal } // 父類不存在該選項,則返回子類的選項
var ret = Object.create(null);
extend(ret, parentVal); //
if (childVal) { extend(ret, childVal); } // 子類選項會覆蓋父類選項的值
return ret
};
複製程式碼
至此,vue初始化選項合併邏輯分析完畢。