為什麼Vue.mixin中的定義的data全域性可用

丁香園F2E發表於2018-08-21

0. 背景

目前在丁香醫生的業務中,我會負責一個基於Vue全家桶的WebApp專案。

一直有件不太弄得明白的事:在每個元件的template標籤裡,都會使用dataReady來進行渲染控制。例如像下面這樣,請求完了以後再渲染頁面。

## 模板部分
<template>
  <div class="wrap"
       v-if="dataReady">
  </div>
</template>

## Script部分

  async created() {
    await this.makeSomeRequest();
    this.dataReady = true;
  },
複製程式碼

但是實際上,我在元件的data選項裡並沒有定義dataReady屬性。

於是,我查了查入口檔案main.js中,有這麼句話

  Vue.mixin({
    data() {
      return {
        dataReady: false
      };
    }
    // 以下省略
  });
複製程式碼

為什麼一個在全域性定義的變數,在每個元件裡都可以用呢?Vue是怎麼做到的呢?

於是,在翻了一堆資料和原始碼之後,有點兒答案了。

1. 前置知識

由於部分前置知識解釋起來很複雜,因此我直接以結論的形式給出:

  • Vue是個建構函式,通過new Vue創造出來的是根例項
  • 所有的單檔案元件,都是通過Vue.extend擴充套件出來的子類。
  • 每個在父元件的標籤中template標籤,或者render函式中渲染的元件,是對應子類的例項。

2. 先從Vue.mixin看起

原始碼長這樣:

  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
複製程式碼

很簡單,把當前上下文物件的options和傳入的引數做一次擴充套件嘛。

所以做事的,其實是mergeOptions這個函式,它把Vue類上的靜態屬性options擴充套件了。

那我們看看mergeOptions,到底做了什麼。

3. Vue類上用mergeOptions進行選項合併

找到mergeOptions原始碼,記住一下。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 中間好長一串程式碼,都跳過不看,暫時和data屬性沒關係。
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    // 檢查是否已經執行過合併,合併過的話,就不需要再次合併了
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
複製程式碼

這個mergeOptions函式,其實就只是在傳入的options物件上,遍歷自身的屬性,來執行mergeField函式,然後返回一個新的options。

那麼問題就變化成了:mergeField到底做了什麼?我們看它的程式碼。

// 找到合併策略函式
const strat = strats[key] || defaultStrat

// 執行合併策略函式
options[key] = strat(parent[key], child[key], vm, key)
複製程式碼

現在回憶一下,

  • parent是什麼?—— 在這個例子裡,是Vue.options
  • child是什麼?對,就是使用mixin方法時傳入的引數物件。
  • 那麼key是什麼? —— 是在parents或者child物件上的某個屬性的鍵。

好,可以確認的是,child物件上,一定包含一個key為data的屬性。

行咯,那我們找找看什麼是strats.data

strats.data = function (
  // parentVal,在這個例子裡,是Vue自身的options選項上的data屬性,有可能不存在
  parentVal: any,
  
  // childVal,在這個例子裡,是mixin方法傳入的選項物件中的data屬性
  childVal: any,
  vm?: Component
): ?Function {

  // 回想一下Vue.mixin的程式碼,會發現vm為空
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      // 這個錯誤眼熟嗎?想想如果你剛才.mixin的時候,傳入的data如果不是函式,是不是就報錯了?
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    
    // 這條語句的返回值,將會在mergeField函式中,作為options.data的值。
    return mergeDataOrFn(parentVal, childVal)
  }
  // 在這個例子裡,下面這行不會執行,為什麼?自己想想。
  return mergeDataOrFn(parentVal, childVal, vm)
}
複製程式碼

OK,那我們再來看看,mergeDataOrFn,到底是什麼。

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // childVal是剛剛mixin方法的引數中的data屬性,一個函式
    if (!childVal) {
      return parentVal
    }
    // parentVal是Vue.options.data屬性,然鵝Vue屬性並沒有自帶的data屬性
    if (!parentVal) {
      return childVal
    }
    // 下邊也不用看了,到這裡就返回了。
  } else {
    // 這裡不用看先,反正你也沒有傳遞vm引數嘛
  }
}
複製程式碼

所以,是不是最終就是這麼句話

Vue.options.data = function data(){
    return {
        dataReady: false
    }
}
複製程式碼

4. 從Vue類 -> 子類

話說,剛剛這個data屬性,明明加在了Vue.options上,憑啥Vue的那些單檔案元件,也就是子類,它們的例項裡也能用啊?

這就要講到Vue.extend函式了,它是用來擴充套件子類的,平時我們寫的一個個SFC單檔案元件,其實都是Vue類的子類。

  Vue.extend = function (extendOptions: Object): Function {
    const Super = this
    
    // 你不用關心中間還有一些程式碼

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    
    // 繼承
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    
    // 注意這裡也執行了options函式,做了選項合併工作。
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    
    // 你不用關心中間還有一些程式碼

    
    // 把子類返回出去了。
    return Sub;
}
複製程式碼
  • extendOptions是什麼?

其實就是我們在單檔案元件裡寫的東西,它可能長這樣

export default {
    // 當然,也可能沒有data函式
    data(){
        return{
            id: 0
        }
    },
    methods: {
        handleClick(){
            
        }
    }
}
複製程式碼
  • Super.options是什麼?

在我們專案裡,是沒有出現Vue -> Parent -> Child這樣的多重繼承關係的,所以可以認為Super.options,就是前面說的Vue.options

記得嗎?在執行完了Vue.mixin之後,Vue.options有data屬性噢。

5. Vue類 -> 子類時的mergeOptions

這時候再來看

Sub.options = mergeOptions(
  Super.options,
  extendOptions
)
複製程式碼

我們再次回到mergeOptions函式。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 省略上面一些檢查和規範化
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  
  // 還是執行策略函式
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
複製程式碼

就和剛才一樣,還是會返回一個options,並且給到Sub.options

其中options.data屬性,仍然會被strats.data策略函式執行一遍,但這次流程未必一樣。

注意,parentValVue.options.data,而childVal可能是一個data函式,也可能為空。為什麼?去問前面的extendOptions啊,它傳的引數啊。

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
        // 省略
    }
    // 沒問題,還是執行這一句。
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}
複製程式碼

我們可以看到,流程基本一致,還是執行return mergeDataOrFn(parentVal, childVal)

我們再看這個mergeDataOrFn

首先假定childVal為空。

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // 到這裡就返回了
    if (!childVal) {
      return parentVal
    }
  } else {
    // 省略
  }
}
複製程式碼

所以如果extendOptions沒傳data屬性(一個函式),那麼他就會使用parentVal,也就是Vue.options.data

所以,可以簡單理解為

Sub.options.data = Vue.options.data = function data(){
    return {
        dataReady: false
    }
}

複製程式碼

那要是extendOptions傳了個data函式呢?我們可以在mergeDataOrFn這個函式裡繼續找

    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
複製程式碼

返回的是個函式,考慮到這裡的childVal和parentVal都是函式,我們可以簡化一下程式碼

// 現在假設子類的data選項長這樣
function subData(){
        return{
            id: 0
        }
}

function vueData(){
    return {
        dataReady: false
    }
}

// Sub得到了什麼?

Sub.options.data = function data(){
    return mergeData(
        subData.call(this, this),
        vueData.call(this, this)
    )
}

複製程式碼

請想一下這裡的this是什麼,在結尾告訴你。

在Sub類進行一次例項化的時候,Sub.options.data會進行執行。所以會得到這個形式的結果。

return mergeData({ id: 0 }, { dataReady: false })
複製程式碼

具體mergeData的原理也很簡單:遍歷key + 深度合併;而如果key同名的話,就不會執行覆蓋。具體的去看下mergeData這個函式好了,這不是本文重點。

具體怎麼執行例項化,怎麼執行data函式的,有興趣的可以自己去了解,簡單說下,和三個函式有關:

  • Vue.prototype._init
  • initState
  • initData

7. 尾聲

現在你理解,為什麼每個元件裡,都會有一個dataReady: false了嗎?

其實一句話概括起來,就是:Vue類上的data函式(我稱為parentDataFn)會與子類的data函式(我稱為childDataFn)合併,得到一個新函式,這個新函式會會在子類在例項化時執行,且同時執行parentDataFn和childDataFn,並返回合併後的data物件。

順便,剛才

Sub.options.data = function mergedDataFn(){
    return mergeData(
        subData.call(this, this),
        vueData.call(this, this)
    )
}
複製程式碼

這裡的this,是一個Sub類的例項。

8. 結語

說實在的,之前會自己在做完工作以後,寫一點文章,讓自己能夠更好地理解自己到底學到了什麼,比如:

但是都是很簡單的“技能記錄”或者“基礎探究”。

而這次,則是第一次嘗試理解像Vue原始碼這樣的複雜系統,很擔心很多地方會誤導人,所以特別感謝以下參考資料:

如果還有什麼說得不太對,還請多提些意見。

最後,丁香醫生前端團隊正在招人。

團隊介紹在這裡

對招聘有意向或者疑問的話,可以在知乎上私信作者。

作者:丁香園 前端工程師 @Kevin Wong

相關文章