爲何Vue.mixin中的定義的data全局可用

0. 背景

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

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

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

## Script部分

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

可是實際上,我在組件的data選項裏並無定義dataReady屬性。前端

因而,我查了查入口文件main.js中,有這麼句話vue

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

爲何一個在全局定義的變量,在每一個組件裏均可以用呢?Vue是怎麼作到的呢?bash

因而,在翻了一堆資料和源碼以後,有點兒答案了。前端工程師

1. 前置知識

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

  • Vue是個構造函數,經過new Vue創造出來的是根實例
  • 全部的單文件組件,都是經過Vue.extend擴展出來的子類。
  • 每一個在父組件的標籤中template標籤,或者render函數中渲染的組件,是對應子類的實例。

2. 先從Vue.mixin看起

源碼長這樣:函數

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

很簡單,把當前上下文對象的options和傳入的參數作一次擴展嘛。post

因此作事的,實際上是mergeOptions這個函數,它把Vue類上的靜態屬性options擴展了。ui

那咱們看看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

相關文章
相關標籤/搜索