最近在做項目的時候碰到了一個奇怪的問題,通過 Vue.mixin 方法注入到 Vue 實例的一個方法不起作用了,后來經過仔細排查發現這個實例自己實現了一個同名方法,導致了 Vue.mixin 注入方法的失效。后來查閱資料發現 Vue.mixin 注入到實例的 methods 方法會被實例中的同名方法替換,而不會依次執行。於是我就有了查看源碼的想法,進而誕生了這篇文章~
本文所用源碼版本為 2.2.6
首先從 Vue.mixin 這個方法入手,打開 src 目錄不難找到 mixin 所在的文件:src/core/global-api/mixin.js,其內容如下:

可以看到這只是一層簡單的封裝,核心內容基本都在 mergeOptions 方法中,所以下面打開這個方法所在的文件:src/core/util/options.js。注意 mergeOptions 方法是通過 src/core/util/index.js 引入導出的,其源碼在 options.js 中,直接看 options.js 就好了。
在 options.js 中找到 mergeOptions 方法,內容如下:

其主流程大致如下:
- 如果是非生產環境下,首先調用
checkComponents檢查傳入參數的合法性,后面再講具體實現。 - 調用
normalizeProps方法和normalizeDirectives方法對這兩個屬性進行規范化。 - 檢查傳入參數是否具有
extends屬性,這個屬性表示擴展其它 Vue 實例,具體參考官方文檔。這里為什么要檢查這個屬性呢?因為當傳入對象具有該屬性時,表示所有的 Vue 實例都要擴展它所指定的實例(Vue.mixin的功能即是如此),那么我們在合並之前,需要先把extends進行合並,如果extends是一個 Vue 構造函數(也可能是擴展后的 Vue 構造函數),那么合並參數變為其options選項了;否則直接合並extends。 - 檢查完傳入參數的
extends屬性之后,我們還要檢查其mixins屬性,這個屬性的功能參考官方文檔。因為如果傳入的 Vue 配置對象仍然指定了mixins的話,我們需要遞歸的進行 merge。 - 做完以上的工作之后,就可以開始合並單純的
mixin參數了。可以看到通過mergeField函數進行了合並,先遍歷合並的目標對象,進行合並了;隨后遍歷要合並的對象,只對目標對象上不存在的屬性進行合並操作。那么合並的重點就到了mergeFiled函數了。
繼續看 mergeField 函數:
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
該函數通過 key 值在 strats 中選取合並的具體函數,這是一種典型的策略模式,所以我們看 strats是如何定義的。
options.js 中關於 strats 的定義如下:
/**
* 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
其中 config 對象來自於 src/core/config.js,它定義了 config 的所有類型及初始值,當然初始值都還是一些空數組之類的,所以我們要在 options.js 中看具體的實現。
下面根據 Vue 的配置屬性分開講解不同的合並方式。
一、el
el 的合並方式比較簡單,因為它本身
源碼如下:
/**
* Options with restrictions
*/
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)
}
}
可以看到這里有個條件,只有在開發環境下才會定義 strats.el 方法以及 propsData 方法(propsData 文檔),這是因為這兩個屬性比較特殊,尤其是 propsData 只在開發環境下才使用,方便測試而已。另外一個比較特殊的地方是這兩者只能在 new 操作符調用 Vue 構造函數所構造的 Vue 實例中才能存在,所以當 vm 未傳遞時,會彈出一個警告。
這兩個屬性的合並方法都是 defaultStrat,其源碼如下:
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
可以看出在 childVal 已定義的時候直接替代 parentVal。
這個方法在后邊還會用到。
二、data
data選項的合並是重中之重,因為 data 在子組件中是一個函數,它返回的也是一個特殊的響應式對象。
其源碼如下:

這里分了兩種情況,一種是傳遞了 vm 參數,一種是沒傳遞。
當沒傳遞 vm 參數的時候,需要校驗 childVal 是否是函數,而 parentVal 不需要校驗,因為它必須是函數才能通過之前的 merge 校驗,到達現在這一步。確定都是函數之后,就調用這兩個函數,再然后對返回的兩個 data 對象通過 mergeData 做處理,這里后面再講。
當傳遞了 vm 參數的時候,需要用其他方式處理,當是函數的時候,使用返回值做下一步合並;當是其他值的時候,直接使用其值進行下一步合並。
這一步要校驗 childVal 和 parentVal 是否為函數。正是因為這一步校驗了,所以前面所講的情況就不再需要校驗,為什么呢?
我們可以回頭看 mergeOptions 的源碼,發現其第三個參數 vm 是可選的,在遞歸的時候它會把 vm 傳遞給自身,這就導致當我們一開始調用 mergeOptions 的時候傳遞了 vm,則其后所有遞歸都會傳遞 vm;當我們一開始未傳遞 vm 值的時候,其后所有的遞歸也不會傳遞 vm 參數。那么是否有 vm 就取決於我們最開始調用該函數時所傳遞的參數是否包含 vm 了。
全局查找 mergeOptions 函數的調用,可以看到有兩處:
- 第一處位於
src/core/instance/init.js,該文件也定義了initMixin方法,用於初始化 Vue 把傳遞給 Vue 構造函數的配置對象合並到 vm.$options 中。這種情況下會傳遞 vm,其值為當前正在構造的 Vue 實例。 - 第二處位於之前一直在講的
src/core/global-api/mixin.js,這處才是定義的全局 API。
簡而言之,Vue 構造函數構造 Vue 實例時,會調用 mergeOptions 並且傳遞 vm 實例作為第三個參數;當我們調用 Vue.mixin 進行全局混淆時是不會傳遞 vm 的。前者對應第二種情況,后者對應第一種情況。
當我們先構造 Vue 實例的時候,vm 被傳遞進而執行第二種情況,parentVal 會被校驗,所以之后再調用 Vue.mixin 時第一種情況不再需要校驗。
當我們先不實例化 Vue 而先調用 Vue.mixin 時,會先執行第一種情況的代碼,那么會導致 bug 出現嗎?答案肯定是不會,因為此時 parentVal 為 undefined,因為 Vue.mixin 調用時 parentVal 的初始值為 Vue.options,這個對象根本不包含 data 屬性。
那么 data 合並的任務主要在 mergeData 函數中了,查看其源碼:

可以看到這里遍歷了要合並的 data 的所有屬性,然后根據不同情況進行合並:
- 當目標 data 對象不包含當前屬性時,調用
set方法進行合並,后面講set。 - 當目標 data 對象包含當前屬性並且當前值為純對象時,遞歸合並當前對象值,這樣做是為了防止對象存在新增屬性。
繼續看 set 函數:

可以看到 set 也對 target 分了兩種情況進行處理。首先判斷了 target 是數組的情況,然后如果 target 包含當前屬性,那么就直接賦值。接下來判斷了 target 是否是響應式對象,如果是的話就會在開發環境下彈出警告,最好不要讓 data 函數返回一個響應式對象,因為會造成性能浪費。如果不是響應式對象也可以直接賦值返回,其他情況下就會進一步轉化 target 為響應式對象,並收集依賴。
以上大概就是 data 的合並方式,可以看出來如果實例指定了與 mixins 相同名稱的 data 值,那么以實例中的為准,mixin 中執行的 data 會失效,如果都是對象但是 mixin 中新增了屬性的話,還是會被添加到實例 data 中去的。
三、生命周期鈎子(Hooks)
Hooks 的合並函數定義為 mergeHook 鈎子,其源碼如下:
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
這個比較簡單,代碼注釋也寫得很清楚了,Vue 實例的生命周期鈎子被合並為一個數組。具體有哪些鈎子可以被合並被寫在 src/core/config.js 中:
/**
* List of lifecycle hooks.
*/
_lifecycleHooks: [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated'
],
合並 assets (components、filters、directives)的方法也比較簡單,下面跳過了。
四、watch
合並 watch 的函數源碼如下:

這一段源碼也很簡單,注釋也很明了,跟生命周期的鈎子一樣,Vue.mixin 會把所有同名的 watch 合並到一個數組中去,在觸發的時候依次執行就好了。
五、props、methods、computed
這三項的合並都使用了相同的策略,源代碼如下:

這里的處理也比較簡單,可以看出來當多次調用 Vue.mixin 混淆時,同名的 props、methods、computed 會被后來者替代;但是當 Vue 構造函數傳遞了同名的屬性時,會以構造函數所接受的配置對象為准。因為 Vue 實例化時也會調用 mergeOptions 第二個參數即為 Vue 構造函數所接受的配置對象,正如前文所述。
六、一些輔助函數
前文有講到幾個輔助函數,比如:checkComponents、normalizeProps、normalizeDirectives。這里簡單貼一下源碼:
checkComponents

這個函數是為了檢查 components 屬性是否符合要求的,主要是防止自定義組件使用 HTML 內置標簽。
normalizeProps

這個函數主要是對 props 屬性進行整理。包括把字符串數組形式的 props 轉換為對象形式,對所有形式的 props 進行格式化整理。
normalizeDirectives

這個函數也主要是對 directives 屬性進行格式化整理的,把原來的對象整理成一個新的符合標准格式的對象。
七、自定義合並策略
看到 Vue 的官方文檔:自定義選項合並策略,它允許我們自定義合並策略,具體方式就是替換 Vue.config.optionsMergeStrategies,也就是前文所提到的那個定義在 src/core/config.js 中的屬性。我們也可以看一下源代碼,這一功能在 src/core/global-api/index.js 文件中的 initGlobalAPI 定義。
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
可以看到最后一句給 Vue 函數定義了一個 config 屬性,其 property 定義為 configDef。在生產環境下不允許設置其值,但是在開發環境下,我們可以直接設置 Vue.config。那么通過設置 Vue.config.optionsMergeStrategies,我們可以改變合並策略,在后面再進行合並操作時,都會讀取 config 對象中的屬性,這時就可以使用我們自定義的合並策略進行合並了。
八、總結
看了這些屬性的合並方式以后,對 Vue.mixin 的工作方式也有了一定的了解了。個人認為基本上可以把 Vue.mixin 合並屬性的方式分為三類,一類是替換式、一類是合並式、還有一類是隊列式。
替換式的有 el、props、methods 和 computed,這一類的行為是新的參數替代舊的參數。
合並式的有 data,這一類的行為是新傳入的參數會被合並到舊的參數中。
隊列式合並的有 watch、所有的生命周期鈎子(hooks),這一類的行為是所有的參數會被合並到一個數組中,必要時再依次取出。
所以對於 Vue.mixin 的使用我們也需要小心,尤其是替換式合並的屬性,當你在 mixins 里面指定了以后,就不要再實例中再指定同名屬性了,那樣的話你的 mixins 中的屬性會被替代導致失效。
作者水平有限,文章難免存在紕漏,敬請大家指正。
