深入剖析Vue源码 - 选项合并(上)

对于大部分的前端开发人员来讲,熟练使用vue做项目是第一步,但当进阶后遇到一些特殊场景,解决棘手问题时,了解vue框架的设计思想和实现思路便是基础需要。本专题将深入vue框架源码,一步步挖掘框架设计理念和思想,并尽可能利用语言将实现思路讲清楚。希望您是在熟练使用vue的前提下阅读此系列文章,也希望您阅读后留下宝贵建议,以便后续文章改进。

<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
var vm = new Vue({
  el: '#app',
  data: {
    message: '选项合并'
  },
  components: {
    'components': {}
  }
})
复制代码

从最简单的使用入手,new一个Vue实例对象是使用vue的第一步,在这一步中,我们需要传递一些基础的选项配置,Vue系统需要进行初始化数据,合并选项的过程。本篇文章我们研究的核心在于各种数据选项在vue的系统中是如何进行合并的(忽略过程中的响应式系统构建)。

// 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属性上。

我们抓取_init方法合并选项的核心部分代码如下:

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等都是检验的重点。下面只会列举components和props的校验讲解,其他的如inject, directive校验类似,请自行对着源码解析。

 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),只能以字母开头等。因此在选项合并之前,需要对规范进行检查。

// 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官方文档,而具体实现如下所示(只简单抽取部分代码):

扫描二维码关注公众号,回复: 5574859 查看本文章
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
}
复制代码

猜你喜欢

转载自juejin.im/post/5c8f40af6fb9a070f90aaf8f