最近在做項目的時候碰到了一個奇怪的問題,通過 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 中的屬性會被替代導致失效。
作者水平有限,文章難免存在紕漏,敬請大家指正。