從源碼看 Vue 中的 Mixin


最近在做項目的時候碰到了一個奇怪的問題,通過 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 方法,內容如下:

其主流程大致如下:

  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 的定義如下:

/**
 * 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 函數的調用,可以看到有兩處:

  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 中去的。

三、生命周期鈎子(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 構造函數所接受的配置對象,正如前文所述。

六、一些輔助函數

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

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 合並屬性的方式分為三類,一類是替換式、一類是合並式、還有一類是隊列式。

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

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

隊列式合並的有 watch、所有的生命周期鈎子(hooks),這一類的行為是所有的參數會被合並到一個數組中,必要時再依次取出。

所以對於 Vue.mixin 的使用我們也需要小心,尤其是替換式合並的屬性,當你在 mixins 里面指定了以后,就不要再實例中再指定同名屬性了,那樣的話你的 mixins 中的屬性會被替代導致失效。

作者水平有限,文章難免存在紕漏,敬請大家指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM