從源碼看 Vue 中的 Mixin

最近在作項目的時候碰到了一個奇怪的問題,經過 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

其主流程大體以下:函數

  1. 若是是非生產環境下,首先調用 checkComponents 檢查傳入參數的合法性,後面再講具體實現。
  2. 調用 normalizeProps 方法和 normalizeDirectives 方法對這兩個屬性進行規範化。
  3. 檢查傳入參數是否具備 extends 屬性,這個屬性表示擴展其它 Vue 實例,具體參考官方文檔。這裏爲何要檢查這個屬性呢?由於當傳入對象具備該屬性時,表示全部的 Vue 實例都要擴展它所指定的實例(Vue.mixin 的功能便是如此),那麼咱們在合併以前,須要先把 extends 進行合併,若是 extends 是一個 Vue 構造函數(也多是擴展後的 Vue 構造函數),那麼合併參數變爲其 options 選項了;不然直接合並 extends
  4. 檢查完傳入參數的 extends 屬性以後,咱們還要檢查其 mixins 屬性,這個屬性的功能參考官方文檔。由於若是傳入的 Vue 配置對象仍然指定了 mixins 的話,咱們須要遞歸的進行 merge。
  5. 作完以上的工做以後,就能夠開始合併單純的 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 的配置屬性分開講解不一樣的合併方式。

1、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

這個方法在後邊還會用到。

2、data

data選項的合併是重中之重,由於 data 在子組件中是一個函數,它返回的也是一個特殊的響應式對象。

其源碼以下:

這裏分了兩種狀況,一種是傳遞了 vm 參數,一種是沒傳遞。

當沒傳遞 vm 參數的時候,須要校驗 childVal 是不是函數,而 parentVal 不須要校驗,由於它必須是函數才能經過以前的 merge 校驗,到達如今這一步。肯定都是函數以後,就調用這兩個函數,再而後對返回的兩個 data 對象經過 mergeData 作處理,這裏後面再講。

當傳遞了 vm 參數的時候,須要用其餘方式處理,當是函數的時候,使用返回值作下一步合併;當是其餘值的時候,直接使用其值進行下一步合併。

這一步要校驗 childValparentVal 是否爲函數。正是由於這一步校驗了,因此前面所講的狀況就再也不須要校驗,爲何呢?

咱們能夠回頭看 mergeOptions 的源碼,發現其第三個參數 vm 是可選的,在遞歸的時候它會把 vm 傳遞給自身,這就致使當咱們一開始調用 mergeOptions 的時候傳遞了 vm,則其後全部遞歸都會傳遞 vm;當咱們一開始未傳遞 vm 值的時候,其後全部的遞歸也不會傳遞 vm 參數。那麼是否有 vm 就取決於咱們最開始調用該函數時所傳遞的參數是否包含 vm 了。

全局查找 mergeOptions 函數的調用,能夠看到有兩處:

  1. 第一處位於 src/core/instance/init.js,該文件也定義了 initMixin 方法,用於初始化 Vue 把傳遞給 Vue 構造函數的配置對象合併到 vm.$options 中。這種狀況下會傳遞 vm,其值爲當前正在構造的 Vue 實例。
  2. 第二處位於以前一直在講的 src/core/global-api/mixin.js,這處纔是定義的全局 API。

簡而言之,Vue 構造函數構造 Vue 實例時,會調用 mergeOptions 而且傳遞 vm 實例做爲第三個參數;當咱們調用 Vue.mixin 進行全局混淆時是不會傳遞 vm 的。前者對應第二種狀況,後者對應第一種狀況。

當咱們先構造 Vue 實例的時候,vm 被傳遞進而執行第二種狀況,parentVal 會被校驗,因此以後再調用 Vue.mixin 時第一種狀況再也不須要校驗。

當咱們先不實例化 Vue 而先調用 Vue.mixin 時,會先執行第一種狀況的代碼,那麼會致使 bug 出現嗎?答案確定是不會,由於此時 parentValundefined,由於 Vue.mixin 調用時 parentVal 的初始值爲 Vue.options,這個對象根本不包含 data 屬性。

那麼 data 合併的任務主要在 mergeData 函數中了,查看其源碼:

能夠看到這裏遍歷了要合併的 data 的全部屬性,而後根據不一樣狀況進行合併:

  1. 當目標 data 對象不包含當前屬性時,調用 set 方法進行合併,後面講 set
  2. 當目標 data 對象包含當前屬性而且當前值爲純對象時,遞歸合併當前對象值,這樣作是爲了防止對象存在新增屬性。

繼續看 set 函數:

能夠看到 set 也對 target 分了兩種狀況進行處理。首先判斷了 target 是數組的狀況,而後若是 target 包含當前屬性,那麼就直接賦值。接下來判斷了 target 是不是響應式對象,若是是的話就會在開發環境下彈出警告,最好不要讓 data 函數返回一個響應式對象,由於會形成性能浪費。若是不是響應式對象也能夠直接賦值返回,其餘狀況下就會進一步轉化 target 爲響應式對象,並收集依賴。

以上大概就是 data 的合併方式,能夠看出來若是實例指定了與 mixins 相同名稱的 data 值,那麼以實例中的爲準,mixin 中執行的 data 會失效,若是都是對象可是 mixin 中新增了屬性的話,仍是會被添加到實例 data 中去的。

3、生命週期鉤子(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)的方法也比較簡單,下面跳過了。

4、watch

合併 watch 的函數源碼以下:

這一段源碼也很簡單,註釋也很明瞭,跟生命週期的鉤子同樣,Vue.mixin 會把全部同名的 watch 合併到一個數組中去,在觸發的時候依次執行就行了。

5、props、methods、computed

這三項的合併都使用了相同的策略,源代碼以下:

這裏的處理也比較簡單,能夠看出來當屢次調用 Vue.mixin 混淆時,同名的 props、methods、computed 會被後來者替代;可是當 Vue 構造函數傳遞了同名的屬性時,會以構造函數所接受的配置對象爲準。由於 Vue 實例化時也會調用 mergeOptions 第二個參數即爲 Vue 構造函數所接受的配置對象,正如前文所述。

6、一些輔助函數

前文有講到幾個輔助函數,好比:checkComponentsnormalizePropsnormalizeDirectives。這裏簡單貼一下源碼:

checkComponents

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

normalizeProps

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

normalizeDirectives

這個函數也主要是對 directives 屬性進行格式化整理的,把原來的對象整理成一個新的符合標準格式的對象。

7、自定義合併策略

看到 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 對象中的屬性,這時就可使用咱們自定義的合併策略進行合併了。

8、總結

看了這些屬性的合併方式之後,對 Vue.mixin 的工做方式也有了必定的瞭解了。我的認爲基本上能夠把 Vue.mixin 合併屬性的方式分爲三類,一類是替換式、一類是合併式、還有一類是隊列式。

替換式的有 elpropsmethodscomputed,這一類的行爲是新的參數替代舊的參數。

合併式的有 data,這一類的行爲是新傳入的參數會被合併到舊的參數中。

隊列式合併的有 watch、全部的生命週期鉤子(hooks),這一類的行爲是全部的參數會被合併到一個數組中,必要時再依次取出。

因此對於 Vue.mixin 的使用咱們也須要當心,尤爲是替換式合併的屬性,當你在 mixins 裏面指定了之後,就不要再實例中再指定同名屬性了,那樣的話你的 mixins 中的屬性會被替代致使失效。

做者水平有限,文章不免存在紕漏,敬請你們指正。

相關文章
相關標籤/搜索