深刻剖析Vue源碼 - 選項合併(下)

1.5 合併策略

合併策略之因此是難點,其中一個是合併選項類型繁多,合併規則隨着選項的不一樣也呈現差別。歸納起來思路主要是如下兩點:vue

  1. Vue針對每一個規定的選項都有定義好的合併策略,例如data,component,mounted等。若是合併的子父配置都具備相同的選項,則只須要按照規定好的策略進行選項合併便可。
  2. 因爲Vue傳遞的選項是開放式的,全部也存在傳遞的選項沒有自定義選項的狀況,這時候因爲選項不存在默認的合併策略,因此處理的原則是有子類配置選項則默認使用子類配置選項,沒有則選擇父類配置選項。

咱們經過這兩個思想去分析源碼的實現,先看看mergeOptions除了規範檢測後的邏輯。算法

function mergeOptions ( parent, child, vm ) {
  ···
  var options = {};
  var key;
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField (key) {
    // 若是有自定義選項策略,則使用自定義選項策略,不然選擇使用默認策略。
    var strat = strats[key] || defaultStrat; 
    options[key] = strat(parent[key], child[key], vm, key);
  }

  return options
}
複製代碼

兩個for循環規定了合併的順序,以自定義選項策略優先,若是沒有才會使用默認策略。而strats下每一個key對應的即是每一個特殊選項的合併策略vue-router

1.5.1 默認策略

咱們能夠用豐富的選項去定義實例的行爲,大體能夠分爲如下幾類:vuex

  1. data,props,computed等選項定義實例數據
  2. mounted, created, destoryed等定義生命週期函數
  3. components註冊組件
  4. methods選項定義實例方法

固然還有諸如watch,inject,directives,filter等選項,總而言之,Vue提供的配置項是豐富的。除此以外,咱們也可使用沒有默認配置策略的選項,典型的例子是狀態管理Vuex和配套路由vue-router的引入:數組

new Vue({
  store, // vuex
  router// vue-router
})
複製代碼

不論是插件也好,仍是用戶自定義的選項,他們的合併策略會遵循思路的第二點:**子配置存在則取子配置,不存在則取父配置,即用子去覆蓋父。。**它的描述在defaultStrat中。瀏覽器

// 用戶自定義選項策略
var defaultStrat = function (parentVal, childVal) {
  // 子不存在則用父,子存在則用子配置
  return childVal === undefined
    ? parentVal
    : childVal
};
複製代碼

接下來會進入某些具體的合併策略的分析,大體分爲五類:app

1. 常規選項合併異步

2. 自帶資源選項合併函數

3. 生命週期鉤子合併源碼分析

4. watch選項合併

5. props,methods, inject, computed相似選項合併

1.6 常規選項的合併

1.6.1 el的合併

el提供一個在頁面上已存在的 DOM 元素做爲 Vue 實例的掛載目標,所以它只在建立Vue實例才存在,在子類或者子組件中沒法定義el選項,所以el的合併策略是在保證選項只存在於根的Vue實例的情形下使用默認策略進行合併。

strats.el = function (parent, child, vm, key) {
  if (!vm) {  // 只容許vue實例才擁有el屬性,其餘子類構造器不容許有el屬性
    warn(
      "option \"" + key + "\" can only be used during instance " +
      'creation with the `new` keyword.'
    );
  }
  // 默認策略
  return defaultStrat(parent, child)
};

複製代碼

1.6.2 data合併

常規選項的重點部分是在於data的合併,讀完這部分源碼,可能能夠解開你心中的一個疑惑,爲何datavue建立實例時傳遞的是一個對象,而在組件內部定義時只能傳遞一個函數。

// data的合併
strats.data = function (parentVal, childVal, vm) {
  // vm表明是否爲Vue建立的實例,不然是子父類的關係
  if (!vm) {
    if (childVal && typeof childVal !== 'function') { // 必須保證子類的data類型是一個函數而不是一個對象
      warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.',vm);
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm); // vue實例時須要傳遞vm做爲函數的第三個參數
};
複製代碼

data策略最終調用的mergeDataOrFn方法,區別在於當前vm是不是實例,或者是單純的子父類的關係。若是是子父類的關係,須要對data選項進行規範校驗,保證它的類型是一個函數而不是對象。

function mergeDataOrFn ( parentVal, childVal, vm ) {
  // 子父類
  if (!vm) {
    if (!childVal) { // 子類不存在data選項,則合併結果爲父類data選項
      return parentVal
    }
    if (!parentVal) { // 父類不存在data選項,則合併結果爲子類data選項
      return childVal
    }
    return function mergedDataFn () { // data選項在父類和子類同時存在的狀況下返回的是一個函數
      // 子類實例和父類實例,分別將子類和父類實例中data函數執行後返回的對象傳遞給mergeData函數作數據合併
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
  // Vue實例
    // vue構造函數實例對象
    return function mergedInstanceDataFn () {
      var instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal;
      var defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal;
      if (instanceData) {
        // 當實例中傳遞data選項時,將實例的data對象和Vm構造函數上的data屬性選項合併
        return mergeData(instanceData, defaultData)
      } else {
        // 當實例中不傳遞data時,默認返回Vm構造函數上的data屬性選項
        return defaultData
      }
    }
  }
}
複製代碼

從源碼的實現看,data的合併非簡單的將兩個數據對象進行合併,而是直接返回一個mergedDataFn或者mergedInstanceDataFn函數,而真正合並的時機是在後續初始化數據響應式系統的環節進行的,初始化數據響應式系統的第一步就是拿到合併後的數據,也就是執行mergeData邏輯。 (關於響應式系統的構建請移步後面的章節)

function mergeData (to, from) {
  if (!from) { return to }
  var key, toVal, fromVal;
  // Reflect.ownKeys能夠拿到Symbol屬性
  var keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from);

  for (var i = 0; i < keys.length; i++) {
    key = keys[i];
    toVal = to[key];
    fromVal = from[key];
    if (!hasOwn(to, key)) {
      // 子的數據父沒有,則將新增的數據加入響應式系統中。
      set(to, key, fromVal); 
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // 處理深層對象,當合並的數據爲多層嵌套對象時,須要遞歸調用mergeData進行比較合併
      mergeData(toVal, fromVal);
    }
  }
  return to
}
複製代碼

mergeData方法的兩個參數是父data選項和子data選項的結果,也就是兩個data對象,從源碼上看數據合併的原則是,將父類的數據整合到子類的數據選項中, 如若父類數據和子類數據衝突時,保留子類數據。若是對象有深層嵌套,則須要遞歸調用mergeData進行數據合併。

最後回過頭來思考一個問題,爲何Vue組件的data是一個函數,而不是一個對象呢? 我以爲能夠這樣解釋:組件設計的目的是爲了複用,每次經過函數建立至關於在一個獨立的內存空間中生成一個data的副本,這樣每一個組件之間的數據不會互相影響。

1.7 自帶資源選項合併

在1.2中咱們看到了Vue默認會帶幾個選項,分別是components組件, directive指令, filter過濾器,全部不管是根實例,仍是父子實例,都須要和系統自帶的資源選項進行合併。它的定義以下:

// 資源選項
var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

// 定義資源合併的策略
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets; // 定義默認策略
});

複製代碼

這些資源選項的合併邏輯很簡單,首先會建立一個原型指向父類資源選項的空對象,再將子類選項賦值給空對象。

// 資源選項自定義合併策略
function mergeAssets (parentVal,childVal,vm,key) {
  var res = Object.create(parentVal || null); // 建立一個空對象,其原型指向父類的資源選項。
  if (childVal) {
    assertObjectType(key, childVal, vm); // components,filters,directives選項必須爲對象
    return extend(res, childVal) // 子類選項賦值給空對象
  } else {
    return res
  }
}
複製代碼

結合下面的例子,咱們看具體合併後的結果:

var vm = new Vue({
  components: {
    componentA: {}
  },
  directives: {
    'v-boom': {}
  }
})

console.log(vm.$options.components)
// 根實例的選項和資源默認選項合併後的結果
{
  components: {
    componentA: {},
    __proto__: {
      KeepAlive: {}
      Transition: {}
      TransitionGroup: {}
    } 
  },
  directives: {
    'v-boom': {},
    __proto__: {
      'v-show': {},
      'v-model': {}
    }
  }
}

複製代碼

簡單總結一下,對於 directives、filters 以及 components 等資源選項,父類選項將以原型鏈的形式被處理。子類必須經過原型鏈才能查找並使用內置組件和內置指令。

1.8 生命週期鉤子函數的合併

在學習Vue時,有一個重要的思想,生命週期。它是咱們使用Vue高效開發組件的基礎,咱們能夠在組件實例的不一樣階段去定義須要執行的函數,讓組件的功能更加豐富。在介紹生命週期鉤子函數的選項合併前,咱們有必要複習如下官方的生命週期圖。

然而從源碼中咱們能夠看到Vue的生命週期鉤子不止這些,它有多達12個之多,每一個鉤子的執行時機咱們暫且不深究,它們會在之後的章節中逐一出現。咱們關心的是:子父組件的生命週期鉤子函數是遵循什麼樣的規則合併。

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
  strats[hook] = mergeHook; // 對生命週期鉤子選項的合併都執行mergeHook策略
});
複製代碼

mergeHook是生命週期鉤子合併的策略,簡單的對代碼進行總結,鉤子函數的合併原則是:

  1. 若是子類和父類都擁有相同鉤子選項,則將子類選項和父類選項合併。
  2. 若是父類不存在鉤子選項,子類存在時,則以數組形式返回子類鉤子選項。
  3. 當子類不存在鉤子選項時,則以父類選項返回。
  4. 子父合併時,是將子類選項放在數組的末尾,這樣在執行鉤子時,永遠是父類選項優先於子類選項執行。
// 生命週期鉤子選項合併策略
function mergeHook ( parentVal, childVal ) {
    // 1.若是子類和父類都擁有鉤子選項,則將子類選項和父類選項合併, 
    // 2.若是父類不存在鉤子選項,子類存在時,則以數組形式返回子類鉤子選項,
    // 3.當子類不存在鉤子選項時,則以父類選項返回。
    var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal; 
    return res
      ? dedupeHooks(res)
      : res
  }
// 防止多個組件實例鉤子選項相互影響
  function dedupeHooks (hooks) {
    var res = [];
    for (var i = 0; i < hooks.length; i++) {
      if (res.indexOf(hooks[i]) === -1) {
        res.push(hooks[i]);
      }
    }
    return res
  }
複製代碼

下面結合具體的例子看合併結果。

var Parent = Vue.extend({
  mounted() {
    console.log('parent')
  }
})
var Child = Parent.extend({
  mounted() {
    console.log('child')
  }
})
var vm = new Child().$mount('#app');

// 輸出結果:
parent
child
複製代碼

簡單總結一下:對於生命週期鉤子選項,子類和父類相同的選項將合併成數組,這樣在執行子類鉤子函數時,父類鉤子選項也會執行,而且父會優先於子執行。

1.9 watch選項合併

在使用Vue進行開發時,咱們有時須要自定義偵聽器來響應數據的變化,當須要在數據變化時執行異步或者開銷較大的操做時,watch每每是高效的。對於 watch 選項的合併處理,它相似於生命週期鉤子,只要父選項有相同的觀測字段,則和子的選項合併爲數組,在監測字段改變時同時執行父類選項的監聽代碼。處理方式和生命鉤子選項的區別在於,生命週期鉤子選項必須是函數,而watch選項最終在合併的數組中能夠是包含選項的對象,也能夠是對應的回調函數,或者方法名。

strats.watch = function (parentVal,childVal,vm,key) {
    //火狐瀏覽器在Object的原型上擁有watch方法,這裏對這一現象作了兼容
    // var nativeWatch = ({}).watch;
    if (parentVal === nativeWatch) { parentVal = undefined; }
    if (childVal === nativeWatch) { childVal = undefined; }
    // 沒有子,則默認用父選項
    if (!childVal) { return Object.create(parentVal || null) }
    {
      // 保證watch選項是一個對象
      assertObjectType(key, childVal, vm);
    }
    // 沒有父則直接用子選項
    if (!parentVal) { return childVal }
    var ret = {};
    extend(ret, parentVal);
    for (var key$1 in childVal) {
      var parent = ret[key$1];
      var child = childVal[key$1];
      // 父的選項先轉換成數組
      if (parent && !Array.isArray(parent)) {
        parent = [parent];
      }
      ret[key$1] = parent
        ? parent.concat(child)
        : Array.isArray(child) ? child : [child];
    }
    return ret
  };
複製代碼

下面結合具體的例子看合併結果:

var Parent = Vue.extend({
  watch: {
    'test': function() {
      console.log('parent change')
    }
  }
})
var Child = Parent.extend({
  watch: {
    'test': {
      handler: function() {
        console.log('child change')
      }
    }
  },
  data() {
    return {
      test: 1
    }
  }
})
var vm = new Child().$mount('#app');
vm.test = 2;
// 輸出結果
parent change
child change
複製代碼

簡單總結一下:對於watch選項的合併,最終和父類選項合併成數組,而且數組的選項成員,能夠是回調函數,選項對象,或者函數名。

1.10 props methods inject computed合併

源碼的設計將props.methods,inject,computed歸結爲一類,他們的配置策略一致,簡單歸納就是,若是父類不存在選項,則返回子類選項,子類父類都存在時,用子類選項去覆蓋父類選項。

// 其餘選項合併策略
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal,childVal,vm,key) {
  if (childVal && "development" !== 'production') {
    assertObjectType(key, childVal, vm);
  }
  if (!parentVal) { return childVal } // 父類不存在該選項,則返回子類的選項
  var ret = Object.create(null);
  extend(ret, parentVal); // 
  if (childVal) { 
    // 子類選項會覆蓋父類選項的值
    extend(ret, childVal); } 
  return ret
};

複製代碼

1.11 小結

至此,五類選項合併的策略分析到此結束,回顧一下這一章節的內容,這一節是Vue源碼分析的起手式,因此咱們從Vue的引入出發,先大體瞭解了Vue在代碼引入階段作的操做,主要是對靜態屬性方法和原型上屬性方法的定義和聲明,這裏並不須要精確瞭解到每一個方法的功能和實現細節,固然我也相信你已經在實戰中或多或少接觸過這些方法的使用。接下來到文章的重點,new Vue是咱們正確使用Vue進行開發的關鍵,而實例化階段會對調用_init方法進行初始化,選項合併是初始化的第一步。選項合併會對系統內部定義的選項和子父類的選項進行合併。而Vue有至關豐富的選項合併策略,不論是內部的選項仍是用戶自定義的選項,他們都遵循內部約定好的合併策略。有了豐富的選項和嚴格的合併策略,Vue在指導開發上才顯得更加完備。下一節會分析一個重要的概念,數據代理,它也是響應式系統的基礎。

相關文章
相關標籤/搜索