深入剖析Vue原始碼 - 選項合併(下)

不做祖國的韭菜發表於2019-03-20

上一節深入剖析Vue原始碼 - 選項合併(上)的末尾,我們介紹了Vue中處理合並選項的思路,概括起來主要有兩點,一是當選項存在定義好的預設配置策略時,優先選擇預設配置策略,並且根據不同的配置項來合併子父選項; 二是當傳入選項不存在預設策略時,處理的原則是有子類配置選項則預設使用子類配置選項,沒有則選擇父類配置選項。vue中,大部分選項都有其自定義策略,因此本節分析的重點也放在了各種自定義配置策略中(內建資源選項,生命週期鉤子選項,el, data, watch, props等)。

首先還是回顧一下選項合併的程式碼,strat這個物件包含了所以自定義的預設策略。

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
}
複製程式碼

1.5 資源選項的自定義策略

在上一節中,我們知道Vue建構函式自身有options的配置選項,分別是components元件, directive指令, filter過濾器,在建立例項之前,程式會將內建元件和內建指令分別掛載到components和directive屬性上。

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];
ASSET_TYPES.forEach(function (type) {
  Vue.options[type + 's'] = Object.create(null); // Vue構造器擁有的預設資源選項配置
});
// Vue內建元件
var builtInComponents = {
  KeepAlive: KeepAlive
};
var platformComponents = {
  Transition: Transition,
  TransitionGroup: TransitionGroup
};
// Vue 內建指令,例如: v-model, v-show
var platformDirectives = {
  model: directive,
  show: show
};
// 將_from物件合併到to物件,屬性相同時,則覆蓋to物件的屬性
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}
extend(Vue.options.components, builtInComponents); 
extend(Vue.options.components, platformComponents); // 擴充套件內建元件
extend(Vue.options.directives, platformDirectives);  // 擴充套件內建指令
複製程式碼

建構函式的預設資源選項配置如下:

Vue.options = {
  components: {
    KeepAlive: {}
    Transition: {}
    TransitionGroup: {}
  },
  directives: {
    model: {inserted: ƒ, componentUpdated: ƒ}
    show: {bind: ƒ, update: ƒ, unbind: ƒ}
  },
  filters: {}
  _base
}
複製程式碼

在例項化Vue,或者例項化子類時,這一類資源選項是如何合併的呢?

// 資源選項自定義合併策略
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
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets; // 定義預設策略
});
複製程式碼

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

1.6 生命週期鉤子選項自定義策略

我們知道掌握vue的生命週期鉤子是使用vue高效開發元件的重點,這是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策略
});

function mergeHook (parentVal,childVal) {
  var res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal; // 1.如果子類和父類都擁有鉤子選項,則將子類選項和父類選項合併, 2如果父類不存在鉤子選項,子類存在時,則以陣列形式返回子類鉤子選項, 3.當子類不存在鉤子選項時,則以父類選項返回。
  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
}
複製程式碼

簡單總結,對於生命週期鉤子選項,子類和父類的選項將合併成陣列,這樣每次執行子類的鉤子函式時,父類鉤子選項也會執行。

1.7 其他自定義策略

Vue自定義選項策略還有很多,我們繼續列舉其他幾個例子。

1.7.1 el合併

我們只在建立vue的例項時才會執行節點掛載,在子類或者子元件中無法定義el選項,程式碼實現如下

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)
};

// 使用者自定義選項策略
var defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
};
複製程式碼
1.7.2 data合併

另一個合併的重點是data的合併策略,data在vue建立例項時傳遞的是一個物件,而在元件內部定義時只能傳遞一個函式,

strats.data = function (parentVal, childVal, vm) {
  if (!vm) {
    if (!vm) {// 判斷是否為Vue建立的例項,否則為子父類的關係
      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建構函式例項物件
    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
      }
    }
  }
}
複製程式碼

如何實現資料合併,資料合併時,vue會將資料變化加入響應式系統中,我們先跳過響應式系統的構建部分,只關注單純的資料合併。資料合併的原則是,將父類的資料整合到子類的資料選項中, 如若父類資料和子類資料衝突時,保留子類資料。

function mergeData (to, from) {
  if (!from) { return to }
  var key, toVal, fromVal;

  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 ( //  處理深層物件,當合並的資料為多層巢狀物件時,需要遞迴呼叫mergeData進行比較合併
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal);
    }
  }
  return to
}
複製程式碼

思考一個問題,為什麼Vue元件的data是一個函式,而不是一個物件呢? 我覺得這樣可以方便理解:元件的目的是為了複用,每次通過函式建立相當於在一個獨立的記憶體空間中生成一個data的副本,這樣每個元件之間的資料不會互相影響。

1.7.3 watch 選項合併

對於 watch 選項的合併處理,類似於生命週期鉤子,只要父選項有相同的觀測欄位,則合併為陣列,在選項改變時同時執行父類選項的監聽程式碼。處理方式和生命鉤子選項的區別在於,生命鉤子選項必須是函式或者資料,而watch選項則為物件。

strats.watch = function (parentVal,childVal,vm,key) {
    if (parentVal === nativeWatch) { parentVal = undefined; }
    if (childVal === nativeWatch) { childVal = undefined; }
    if (!childVal) { return Object.create(parentVal || null) }
    {
      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
  };
複製程式碼
1.7.4 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
};

複製程式碼

至此,vue初始化選項合併邏輯分析完畢。



相關文章