1.5 合併策略
合併策略之所以是難點,其中一個是合併選項型別繁多,合併規則隨著選項的不同也呈現差異。概括起來思路主要是以下兩點:
Vue
針對每個規定的選項都有定義好的合併策略,例如data,component,mounted
等。如果合併的子父配置都具有相同的選項,則只需要按照規定好的策略進行選項合併即可。- 由於
Vue
傳遞的選項是開放式的,所有也存在傳遞的選項沒有自定義選項的情況,這時候由於選項不存在預設的合併策略,所以處理的原則是有子類配置選項則預設使用子類配置選項,沒有則選擇父類配置選項。
我們通過這兩個思想去分析原始碼的實現,先看看mergeOptions
除了規範檢測後的邏輯。
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
}
複製程式碼
兩個for
迴圈規定了合併的順序,以自定義選項策略優先,如果沒有才會使用預設策略。而strats
下每個key
對應的便是每個特殊選項的合併策略
1.5.1 預設策略
我們可以用豐富的選項去定義例項的行為,大致可以分為以下幾類:
- 用
data,props,computed
等選項定義例項資料 - 用
mounted, created, destoryed
等定義生命週期函式 - 用
components
註冊元件 - 用
methods
選項定義例項方法
當然還有諸如watch,inject,directives,filter
等選項,總而言之,Vue
提供的配置項是豐富的。除此之外,我們也可以使用沒有預設配置策略的選項,典型的例子是狀態管理Vuex
和配套路由vue-router
的引入:
new Vue({
store, // vuex
router// vue-router
})
複製程式碼
不管是外掛也好,還是使用者自定義的選項,他們的合併策略會遵循思路的第二點:**子配置存在則取子配置,不存在則取父配置,即用子去覆蓋父。。**它的描述在defaultStrat
中。
// 使用者自定義選項策略
var defaultStrat = function (parentVal, childVal) {
// 子不存在則用父,子存在則用子配置
return childVal === undefined
? parentVal
: childVal
};
複製程式碼
接下來會進入某些具體的合併策略的分析,大致分為五類:
1. 常規選項合併
2. 自帶資源選項合併
3. 生命週期鉤子合併
4. watch
選項合併
5. props,methods, inject, computed
類似選項合併
1.6 常規選項的合併
1.6.1 el的合併
el
提供一個在頁面上已存在的 DOM
元素作為 Vue
例項的掛載目標,因此它只在建立Vue
例項才存在,在子類或者子元件中無法定義el
選項,因此el
的合併策略是在保證選項只存在於根的Vue
例項的情形下使用預設策略進行合併。
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)
};
複製程式碼
1.6.2 data合併
常規選項的重點部分是在於data
的合併,讀完這部分原始碼,可能可以解開你心中的一個疑惑,為什麼data
在vue
建立例項時傳遞的是一個物件,而在元件內部定義時只能傳遞一個函式。
// data的合併
strats.data = function (parentVal, childVal, vm) {
// vm代表是否為Vue建立的例項,否則是子父類的關係
if (!vm) {
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例項
// 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
}
}
}
}
複製程式碼
從原始碼的實現看,data
的合併不是簡單的將兩個資料物件進行合併,而是直接返回一個mergedDataFn
或者mergedInstanceDataFn
函式,而真正合並的時機是在後續初始化資料響應式系統的環節進行的,初始化資料響應式系統的第一步就是拿到合併後的資料,也就是執行mergeData
邏輯。
(關於響應式系統的構建請移步後面的章節)
function mergeData (to, from) {
if (!from) { return to }
var key, toVal, fromVal;
// Reflect.ownKeys可以拿到Symbol屬性
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 (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 處理深層物件,當合並的資料為多層巢狀物件時,需要遞迴呼叫mergeData進行比較合併
mergeData(toVal, fromVal);
}
}
return to
}
複製程式碼
mergeData
方法的兩個引數是父data
選項和子data
選項的結果,也就是兩個data
物件,從原始碼上看資料合併的原則是,將父類的資料整合到子類的資料選項中, 如若父類資料和子類資料衝突時,保留子類資料。如果物件有深層巢狀,則需要遞迴呼叫mergeData
進行資料合併。
最後回過頭來思考一個問題,為什麼Vue
元件的data
是一個函式,而不是一個物件呢?
我覺得可以這樣解釋:元件設計的目的是為了複用,每次通過函式建立相當於在一個獨立的記憶體空間中生成一個data
的副本,這樣每個元件之間的資料不會互相影響。
1.7 自帶資源選項合併
在1.2中我們看到了Vue
預設會帶幾個選項,分別是components
元件, directive
指令, filter
過濾器,所有無論是根例項,還是父子例項,都需要和系統自帶的資源選項進行合併。它的定義如下:
// 資源選項
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
// 定義資源合併的策略
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets; // 定義預設策略
});
複製程式碼
這些資源選項的合併邏輯很簡單,首先會建立一個原型指向父類資源選項的空物件,再將子類選項賦值給空物件。
// 資源選項自定義合併策略
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
}
}
複製程式碼
結合下面的例子,我們看具體合併後的結果:
var vm = new Vue({
components: {
componentA: {}
},
directives: {
'v-boom': {}
}
})
console.log(vm.$options.components)
// 根例項的選項和資源預設選項合併後的結果
{
components: {
componentA: {},
__proto__: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
}
},
directives: {
'v-boom': {},
__proto__: {
'v-show': {},
'v-model': {}
}
}
}
複製程式碼
簡單總結一下,對於 directives、filters
以及 components
等資源選項,父類選項將以原型鏈的形式被處理。子類必須通過原型鏈才能查詢並使用內建元件和內建指令。
1.8 生命週期鉤子函式的合併
在學習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策略
});
複製程式碼
mergeHook
是生命週期鉤子合併的策略,簡單的對程式碼進行總結,鉤子函式的合併原則是:
- 如果子類和父類都擁有相同鉤子選項,則將子類選項和父類選項合併。
- 如果父類不存在鉤子選項,子類存在時,則以陣列形式返回子類鉤子選項。
- 當子類不存在鉤子選項時,則以父類選項返回。
- 子父合併時,是將子類選項放在陣列的末尾,這樣在執行鉤子時,永遠是父類選項優先於子類選項執行。
// 生命週期鉤子選項合併策略
function mergeHook (
parentVal,
childVal
) {
// 1.如果子類和父類都擁有鉤子選項,則將子類選項和父類選項合併,
// 2.如果父類不存在鉤子選項,子類存在時,則以陣列形式返回子類鉤子選項,
// 3.當子類不存在鉤子選項時,則以父類選項返回。
var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal;
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
}
複製程式碼
下面結合具體的例子看合併結果。
var Parent = Vue.extend({
mounted() {
console.log('parent')
}
})
var Child = Parent.extend({
mounted() {
console.log('child')
}
})
var vm = new Child().$mount('#app');
// 輸出結果:
parent
child
複製程式碼
簡單總結一下:對於生命週期鉤子選項,子類和父類相同的選項將合併成陣列,這樣在執行子類鉤子函式時,父類鉤子選項也會執行,並且父會優先於子執行。
1.9 watch選項合併
在使用Vue
進行開發時,我們有時需要自定義偵聽器來響應資料的變化,當需要在資料變化時執行非同步或者開銷較大的操作時,watch
往往是高效的。對於 watch
選項的合併處理,它類似於生命週期鉤子,只要父選項有相同的觀測欄位,則和子的選項合併為陣列,在監測欄位改變時同時執行父類選項的監聽程式碼。處理方式和生命鉤子選項的區別在於,生命週期鉤子選項必須是函式,而watch
選項最終在合併的陣列中可以是包含選項的物件,也可以是對應的回撥函式,或者方法名。
strats.watch = function (parentVal,childVal,vm,key) {
//火狐瀏覽器在Object的原型上擁有watch方法,這裡對這一現象做了相容
// var nativeWatch = ({}).watch;
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
// 沒有子,則預設用父選項
if (!childVal) { return Object.create(parentVal || null) }
{
// 保證watch選項是一個物件
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
};
複製程式碼
下面結合具體的例子看合併結果:
var Parent = Vue.extend({
watch: {
'test': function() {
console.log('parent change')
}
}
})
var Child = Parent.extend({
watch: {
'test': {
handler: function() {
console.log('child change')
}
}
},
data() {
return {
test: 1
}
}
})
var vm = new Child().$mount('#app');
vm.test = 2;
// 輸出結果
parent change
child change
複製程式碼
簡單總結一下:對於watch選項的合併,最終和父類選項合併成陣列,並且陣列的選項成員,可以是回撥函式,選項物件,或者函式名。
1.10 props methods inject computed合併
原始碼的設計將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
};
複製程式碼
1.11 小結
至此,五類選項合併的策略分析到此結束,回顧一下這一章節的內容,這一節是Vue
原始碼分析的起手式,所以我們從Vue
的引入出發,先大致瞭解了Vue
在程式碼引入階段做的操作,主要是對靜態屬性方法和原型上屬性方法的定義和宣告,這裡並不需要精確瞭解到每個方法的功能和實現細節,當然我也相信你已經在實戰中或多或少接觸過這些方法的使用。接下來到文章的重點,new Vue
是我們正確使用Vue
進行開發的關鍵,而例項化階段會對呼叫_init
方法進行初始化,選項合併是初始化的第一步。選項合併會對系統內部定義的選項和子父類的選項進行合併。而Vue
有相當豐富的選項合併策略,不管是內部的選項還是使用者自定義的選項,他們都遵循內部約定好的合併策略。有了豐富的選項和嚴格的合併策略,Vue
在指導開發上才顯得更加完備。下一節會分析一個重要的概念,資料代理,它也是響應式系統的基礎。
- 深入剖析Vue原始碼 - 選項合併(上)
- 深入剖析Vue原始碼 - 選項合併(下)
- 深入剖析Vue原始碼 - 資料代理,關聯子父元件
- 深入剖析Vue原始碼 - 例項掛載,編譯流程
- 深入剖析Vue原始碼 - 完整渲染過程
- 深入剖析Vue原始碼 - 元件基礎
- 深入剖析Vue原始碼 - 元件進階
- 深入剖析Vue原始碼 - 響應式系統構建(上)
- 深入剖析Vue原始碼 - 響應式系統構建(中)
- 深入剖析Vue原始碼 - 響應式系統構建(下)
- 深入剖析Vue原始碼 - 來,跟我一起實現diff演算法!
- 深入剖析Vue原始碼 - 揭祕Vue的事件機制
- 深入剖析Vue原始碼 - Vue插槽,你想了解的都在這裡!
- 深入剖析Vue原始碼 - 你瞭解v-model的語法糖嗎?
- 深入剖析Vue原始碼 - Vue動態元件的概念,你會亂嗎?
- 徹底搞懂Vue中keep-alive的魔法(上)
- 徹底搞懂Vue中keep-alive的魔法(下)