最近在作項目的時候碰到了一個奇怪的問題,經過 Vue.mixin
方法注入到 Vue 實例的一個方法不起做用了,後來通過仔細排查發現這個實例本身實現了一個同名方法,致使了 Vue.mixin
注入方法的失效。後來查閱資料發現 Vue.mixin
注入到實例的 methods
方法會被實例中的同名方法替換,而不會依次執行。因而我就有了查看源碼的想法,進而誕生了這篇文章~html
本文所用源碼版本爲 2.2.6vue
首先從 Vue.mixin
這個方法入手,打開 src
目錄不難找到 mixin
所在的文件:src/core/global-api/mixin.js
,其內容以下:api
能夠看到這只是一層簡單的封裝,核心內容基本都在 mergeOptions
方法中,因此下面打開這個方法所在的文件:src/core/util/options.js
。注意 mergeOptions
方法是經過 src/core/util/index.js
引入導出的,其源碼在 options.js
中,直接看 options.js
就行了。數組
在 options.js
中找到 mergeOptions
方法,內容以下:ide
其主流程大體以下:函數
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
的定義以下:ui
/** * 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
中看具體的實現。spa
下面根據 Vue 的配置屬性分開講解不一樣的合併方式。
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
在子組件中是一個函數,它返回的也是一個特殊的響應式對象。
其源碼以下:
這裏分了兩種狀況,一種是傳遞了 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 的全部屬性,而後根據不一樣狀況進行合併:
set
方法進行合併,後面講 set
。繼續看 set
函數:
能夠看到 set
也對 target 分了兩種狀況進行處理。首先判斷了 target 是數組的狀況,而後若是 target 包含當前屬性,那麼就直接賦值。接下來判斷了 target 是不是響應式對象,若是是的話就會在開發環境下彈出警告,最好不要讓 data 函數返回一個響應式對象,由於會形成性能浪費。若是不是響應式對象也能夠直接賦值返回,其餘狀況下就會進一步轉化 target 爲響應式對象,並收集依賴。
以上大概就是 data 的合併方式,能夠看出來若是實例指定了與 mixins 相同名稱的 data 值,那麼以實例中的爲準,mixin 中執行的 data 會失效,若是都是對象可是 mixin 中新增了屬性的話,仍是會被添加到實例 data 中去的。
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
的函數源碼以下:
這一段源碼也很簡單,註釋也很明瞭,跟生命週期的鉤子同樣,Vue.mixin
會把全部同名的 watch 合併到一個數組中去,在觸發的時候依次執行就行了。
這三項的合併都使用了相同的策略,源代碼以下:
這裏的處理也比較簡單,能夠看出來當屢次調用 Vue.mixin 混淆時,同名的 props、methods、computed 會被後來者替代;可是當 Vue 構造函數傳遞了同名的屬性時,會以構造函數所接受的配置對象爲準。由於 Vue 實例化時也會調用 mergeOptions 第二個參數即爲 Vue 構造函數所接受的配置對象,正如前文所述。
前文有講到幾個輔助函數,好比:checkComponents
、normalizeProps
、normalizeDirectives
。這裏簡單貼一下源碼:
這個函數是爲了檢查 components 屬性是否符合要求的,主要是防止自定義組件使用 HTML 內置標籤。
這個函數主要是對 props 屬性進行整理。包括把字符串數組形式的 props 轉換爲對象形式,對全部形式的 props 進行格式化整理。
這個函數也主要是對 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 中的屬性會被替代致使失效。
做者水平有限,文章不免存在紕漏,敬請你們指正。