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

對於大部分的前端開發人員來說,熟練使用 vue作項目是第一步,但當進階後遇到一些特殊場景,解決棘手問題時,瞭解 vue框架的設計思想和實現思路即是基礎須要。本專題將深刻 vue框架源碼,一步步挖掘框架設計理念和思想,並儘量利用語言將實現思路講清楚。但願您是在熟練使用 vue的前提下閱讀此係列文章,也但願您閱讀後能留下寶貴建議,以便後續文章改進。
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.8/dist/vue.js"></script>
var vm = new Vue({
  el: '#app',
  data: {
    message: '選項合併'
  },
  components: {
    'components': {}
  }
})

從最簡單的使用入手,new一個Vue實例對象是使用vue的第一步,在這一步中,咱們須要傳遞一些基礎的選項配置,Vue會根據系統的默認選項和用戶自定選項進行合併選項配置的過程。本系列將從這一過程展開,在這一節中咱們研究的核心在於各類數據選項在vue系統中是如何進行合併的(忽略過程當中的響應式系統構建,後面專題講解)。html

// Vue 構造函數
function Vue (options) {
  if (!(this instanceof Vue)
  ) {
    // 規定vue只能經過new實例化建立,不然拋出異常
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

// 在引進Vue時,會執行initMixin方法,該方法會在Vue的原型上定義數據初始化init方法,方法只在實例化Vue時執行。
initMixin(Vue);

// 暫時忽略其餘初始化過程。。。
···

接下來,咱們將圍繞vue數據的初始化展開解析。前端

1.1 Vue構造器的默認選項

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];
Vue.options = Object.create(null); // 原型上建立了一個指向爲空對象的options屬性
ASSET_TYPES.forEach(function (type) {
  Vue.options[type + 's'] = Object.create(null);
});
Vue.options._base = Vue;

Vue構造函數自身有四個默認配置選項,分別是component,directive, filter以及返回自身構造器的_base(這裏先不展開對每一個屬性內容的介紹)。這四個屬性掛載在構造函數的options屬性上。vue

咱們抓取_init方法合併選項的核心部分代碼以下:html5

function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      // 記錄實例化多少個vue對象
      vm._uid = uid$3++;

      // 選項合併,將合併後的選項賦值給實例的$options屬性
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // 返回Vue構造函數自身的配置項
        options || {},
        vm
      );
    };
  }

從代碼中能夠看到,選項合併的重點是將用戶自身傳遞的options選項和Vue構造函數自身的選項配置合併,並將合併結果掛載到實例對象的$options屬性上。算法

1.2 選項校驗

選項合併過程咱們更多的不可控在於不知道用戶傳了哪些配置選項,這些配置是否符合規範,因此每一個選項的規範須要嚴格定義好,不容許用戶按照規範外的標準來傳遞選項。所以在合併選項以前,很大的一部分工做是對選項的校驗。其中components,prop,inject,directive等都是檢驗的重點。下面只會列舉componentsprops的校驗講解,其餘的如inject, directive校驗相似,請自行對着源碼解析。npm

function mergeOptions ( parent, child, vm ) {
    {
      checkComponents(child); // 合併前對選項components進行規範檢測
    }

    if (typeof child === 'function') {
      child = child.options;
    }

    normalizeProps(child, vm); // 校驗props選項
    normalizeInject(child, vm); // 校驗inject選項
    normalizeDirectives(child); // 校驗directive選項

    if (!child._base) {
      if (child.extends) {
        parent = mergeOptions(parent, child.extends, vm);
      }
      if (child.mixins) {
        for (var i = 0, l = child.mixins.length; i < l; i++) {
          parent = mergeOptions(parent, child.mixins[i], 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.2.1 components規範檢驗

咱們能夠在vue實例化時傳入組件選項以此來註冊組件。所以,組件命名須要遵照不少規範,好比組件名不能用html保留的標籤(如:img,p),只能以字母開頭等。所以在選項合併以前,須要對規範進行檢查。api

// components規範檢查函數
function checkComponents (options) {
  for (var key in options.components) {
    validateComponentName(key);
  }
}
function validateComponentName (name) {
  if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
    // 正則判斷檢測是否爲非法的標籤
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    );
  }
  // 不能使用Vue自身自定義的組件名,如slot, component,不能使用html的保留標籤,如 h1, svg等
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    );
  }
}

1.2.2 props規範檢驗

vue的使用文檔看,props選項的形式有兩種,一種是['a', 'b', 'c']的數組形式,一種是{ a: { type: 'String', default: 'hahah' }}帶有校驗規則的形式。從源碼上看,兩種形式最終都會轉換成對象的形式。數組

// props規範校驗
  function normalizeProps (options, vm) {
    var props = options.props;
    if (!props) { return }
    var res = {};
    var i, val, name;
    // props選項數據有兩種形式,一種是['a', 'b', 'c'],一種是{ a: { type: 'String', default: 'hahah' }}
    if (Array.isArray(props)) {
      i = props.length;
      while (i--) {
        val = props[i];
        if (typeof val === 'string') {
          name = camelize(val);
          res[name] = { type: null }; // 默認將數組形式的props轉換爲對象形式。
        } else {
          // 保證是字符串
          warn('props must be strings when using array syntax.');
        }
      }
    } else if (isPlainObject(props)) {
      for (var key in props) {
        val = props[key];
        name = camelize(key);
        res[name] = isPlainObject(val)
          ? val
          : { type: val };
      }
    } else {
      // 非數組,非對象則斷定props選項傳遞非法
      warn(
        "Invalid value for option \"props\": expected an Array or an Object, " +
        "but got " + (toRawType(props)) + ".",
        vm
      );
    }
    options.props = res;
  }

1.2.3 函數緩存

在讀到props規範檢驗時,我發現了一段函數優化的代碼,他將每次執行函數後的值緩存起來,下次重複執行的時候調用緩存的數據,以此提升前端性能,這是典型的偏函數應用,能夠參考我另外一篇文章打造屬於本身的underscore系列(五)- 偏函數和函數柯里化緩存

function cached (fn) {
  var cache = Object.create(null); // 建立空對象做爲緩存對象
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str)) // 每次執行時緩存對象有值則不須要執行函數方法,沒有則執行並緩存起來
  })
}

var camelize = cached(function (str) {
  // 將諸如 'a-b'的寫法統一處理成駝峯寫法'aB'
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});

1.3 子類構造器

選項校驗介紹完後,在正式進入合併策略以前,還須要先了解一個東西,子類構造器。在vue的應用實例中,咱們經過Vue.extend({ template: '<div></div>', data: function() {} })建立一個子類,這個子類和Vue實例建立的父類同樣,能夠經過建立實例並掛載到具體的一個元素上。具體用法詳見Vue官方文檔,而具體實現以下所示(只簡單抽取部分代碼):app

Vue.extend = function (extendOptions) {
  extendOptions = extendOptions || {};
  var Super = this;

  var name = extendOptions.name || Super.options.name;
  if (name) {
    validateComponentName(name); // 校驗子類的名稱是否符合規範
  }

  var Sub = function VueComponent (options) { // 子類構造器
    this._init(options);
  };
  Sub.prototype = Object.create(Super.prototype); // 子類繼承於父類
  Sub.prototype.constructor = Sub;
  Sub.cid = cid++;
  // 子類和父類構造器的配置選項進行合併
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  );

  return Sub // 返回子類構造函數
};

爲何要先介紹子類構造器的概念呢,緣由是在選項合併的代碼中,除了須要合併Vue實例和Vue構造器自身的配置,還須要合併子類構造器和父類構造器選項的場景。

1.4 合併策略

合併策略之因此是難點,其中一個是合併選項類型繁多,大致能夠分爲如下三類:Vue自定義策略, 父類自身配置, 子類自身策略(用戶配置)。如何理解?

  • Vue自定義策略,vue在選項合併的時候對一些特殊的選項有自身定義好的合併策略,例如data的合併,el的合併,而每個的合併規則都不同,所以須要對每個規定選項進行特殊的合併處理
  • 父類自身配置,首先建立一個vue實例時,Vue構造函數自身的options屬於父類自身配置,咱們須要將實例傳遞的配置和Vue.options進行合併。再者前面提到的var P = Vue.extends(); var C = P.extends(),P做爲C的父類,在合併選項時一樣須要考慮進去。
  • 子類自身策略(用戶配置),用戶自身選項也就是經過new 實例傳遞的options選項

Vue源碼中,如何處理好這三個選項的合併,思路是這樣的:

  1. 首選默認自定義策略,根據不一樣選項的策略合併子和父的配置項
  2. 不存在自定義策略時,有子類配置選項則默認使用子類配置選項,沒有則選擇父類配置選項。
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
}

喜歡本系列的朋友歡迎關注公衆號 假前端,有源碼解析和算法精選哦

相關文章
相關標籤/搜索