Vue2.X源码学习笔记(一)选项合并

在页面中引入Vue的时候,让我们来看看在Vue源码内部会发生什么。

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.Vue = factory());
}(this, function () { 
    'use strict';
     /******/
    //此处省略多行代码
    return Vue;         
}

  首先看上面的代码,代码的重点是,这是一个立即执行函数,在经过省略代码部分的一系列的操作后,将Vue构造器通过exports或者define或者global等三种方式抛出,提供我们调用,这三种方式涉及到UMD规范,commonjs,amd等知识点,这不是重点,所以一笔带过,总而言之这个立即执行函数的作用就是将Vue构造器暴露出来,供我们使用。既是我们熟悉的new Vue({....})操作。

  当在页面中使用new Vue({...})创建实例时,让我们来看看会发生什么。

function Vue (options) {
  /* 这个函数不能被直接调用,如果被直接调用,由于是严格模式下,this指向undefined,不是vue的实例,抛出警告 */ if (!(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }

  以上代码是Vue实例的构造函数,当我们实例化Vue后,会执行_init方法,options参数是我们实例化操作的时候传入的对象,如下.。

var vm = new Vue({
  el: '#app',
  data: {
    message: '选项合并'
  },
}) /* el,data等属性便是这个options对象的属性 */

  这个_init方法在之前的立即执行函数执行并抛出Vue构造器时便已经通过initMixin(Vue)这一步操作定义,让我们来看看InitMixin方法。

function initMixin (Vue) {
  /*这个方法将Vue构造器对象当做参数传入,并在其原型对象prototype上定义_init方法 */ Vue.prototype._init = function (options) {
    //vm指向Vue实例 var vm = this; // a uid
    //记录多少个Vue实例化对象 vm._uid = uid$3++; var startTag, endTag;     //性能检测和打点标记,这里先略过 if (config.performance && mark) { startTag = "vue-perf-start:" + (vm._uid); endTag = "vue-perf-end:" + (vm._uid); mark(startTag); } // a flag to avoid this being observed vm._isVue = true; // merge options

    //传入的对象为组件时,这里暂时不分析,进入else代码块 if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options); } else {
     //当new Vue({...})生成实例时,会进行选项的合并 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } /* istanbul ignore else */ { initProxy(vm); } // expose real self vm._self = vm; initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); /* istanbul ignore if */ if (config.performance && mark) { vm._name = formatComponentName(vm, false); mark(endTag); measure(("vue " + (vm._name) + " init"), startTag, endTag); } if (vm.$options.el) { vm.$mount(vm.$options.el); } }; }

  当我们创建一个Vue实例时,会调用mergeOptions方法进行选项的合并,先来看看第一个参数,既执行resolveConstructorOptions(vm.constructor)后的返回值,vm.constructor指向Vue实例的构造函数。

function resolveConstructorOptions (Ctor) {
   var options = Ctor.options;/** 代码1 **/
   //如果Ctor参数为子构造器时执行,这里先不深究 if (Ctor.super) { var superOptions = resolveConstructorOptions(Ctor.super); var cachedSuperOptions = Ctor.superOptions; if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions; // check if there are any late-modified/attached options (#4976) var modifiedOptions = resolveModifiedOptions(Ctor); // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions); } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions); if (options.name) { options.components[options.name] = Ctor; } } } return options }

  上面的方法返回的是Vue构造器的默认选项,这些选项在Vue构造器抛出之前便已经定义好了,我们回到选项合并的操作。

vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
  );

  第二个参数为实例化Vue时候传入的对象,前文有提到过,第三个参数为当前的Vue实例。我们再来看看选项合并的核心mergeOptions函数。

function mergeOptions (
    parent,//既Vue构造器默认的选项
    child, //Vue实例的选项
    vm //vue实例
  ) {
    {
   //首先进行Vue实例组件名称的检验 checkComponents(child); } if (typeof child === 'function') { child = child.options; } normalizeProps(child, vm); normalizeInject(child, vm); normalizeDirectives(child); // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property.
  //针对extend扩展的子类构造器。
    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
  }

  当执行合并选项逻辑时,会先调用checkComponents方法,将实例选项当做参数传入,检查实例选项的components对象的组件名是否合法。检验逻辑并不复杂,一是不能以非法字符开头,比如数字,下划线,'-'开头的组件名,二是不能用Vue自身定义的的组件名,比如slot,component,三是不能用html的保留标签,如h1,svg等。

  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.'
      );
    }
    if (isBuiltInTag(name) || config.isReservedTag(name)) {
      warn(
        'Do not use built-in or reserved HTML elements as component ' +
        'id: ' + name
      );
    }
  }

   调用完checkComponents方法后,紧接着调用normalizeProps方法进行props选项的合法校验。

function normalizeProps (options, vm) {
    var props = options.props;
    if (!props) { return }
    var res = {};
    var i, val, name;
    if (Array.isArray(props)) {
      i = props.length;
      while (i--) {
        val = props[i];
        if (typeof val === 'string') {
          name = camelize(val);
          res[name] = { type: null };
        } 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 {
      warn(
        "Invalid value for option \"props\": expected an Array or an Object, " +
        "but got " + (toRawType(props)) + ".",
        vm
      );
    }
    options.props = res;
  }

  如果传入的props选项为数组,那么normalizeProps方法会对数组里的值进行一一校验,保证其为字符串类型,并将该数组中的值val转换为我们熟悉的 val: {type: xxx, default: xxx}并缓存至res对象里, 其他类型,比如数组,对象,Number等会无法通过校验,这里重点理解一下camelize方法。

 /**
   * Create a cached version of a pure function.
   */
  function cached (fn) {
    var cache = Object.create(null);
    return (function cachedFn (str) {
      var hit = cache[str];
      return hit || (cache[str] = fn(str))
    })
  }

  /**
   * Camelize a hyphen-delimited string.
   */
  var camelizeRE = /-(\w)/g;
  var camelize = cached(function (str) {
    return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
  });

  在Vue构造器抛出时,就已经执行了一遍cached方法,该方法接收一个将字符串中的'-'转换为空字符串并且将该字符串的字符变为大写的函数。

  在cached方法内部,首先创建一个空对象cache,然后返回一个cachedFn 方法,cachedFn方法里面保留了对父级作用域中cache对象以及字符串转换函数fn的引用。

   当我们调用camelize(str)时,实际上就是把str的值作为参数传入cachedFn方法里,第一次执行cachedFn时,在cache对象中,并没有key为str的value,所以hit被赋予undefined,此时执行fn(str)函数将str转换为最终结果返回,并在cache中缓存该key为str的value。

  当第二次调用camelize(str)时,由于cachedFn方法保留了对cache对象的引用,cache对象并没有销毁,str作为key能在cache对象中找到value值时,直接赋值给hit返回,就不用再次调用字符串转换函数fn,这样可以节省一部分性能。

  这里其实就是运用到了偏函数以及闭包的概念,通过传入一个绑定函数(字符串处理函数fn),返回一个函数(cacheFn),且该函数保留了对父级作用域的变量,对象引用(cache,fn),在调用该函数时,根据传入的参数去寻找所保留的引用中是否包含已经通过该参数计算过的结果,如果有直接将其返回,如果没有则执行绑定函数(fn),关于偏函数和闭包的概念大家可以自行百度了解。

  以上篇幅我们分析了当props为数组时,normalizeProps方法会把数组中的值转换为一个个对象,存在res中,我们接下来看看当props选项为对象时会发生什么。

var _toString = Object.prototype.toString;

function isPlainObject (obj) {
    return _toString.call(obj) === '[object Object]'
}

/****

省略.......

****/
else if (isPlainObject(props)) {
    for (var key in props) {
        val = props[key];
        name = camelize(key);
        res[name] = isPlainObject(val)
          ? val
          : { type: val };
    }
} 

  isPlainObject方法判断props选项是否为对象,如果为对象,则对该对象下每个key值通过camelize方法转换,并判断每个key对应value是否为对象,如果是则将value存到res,如果不是,则将其转换为{type: xxx}的形式存进res。然后,将规范化后的结果res重新赋值到props选项。

  normalizeProps方法执行完后,会继续执行normalizeInject方法,该方法和normalizeProps的验证过程大同小异,这里不再赘述。

function normalizeInject (options, vm) {
    var inject = options.inject;
    if (!inject) { return }
    var normalized = options.inject = {};
    if (Array.isArray(inject)) {
      for (var i = 0; i < inject.length; i++) {
        normalized[inject[i]] = { from: inject[i] };
      }
    } else if (isPlainObject(inject)) {
      for (var key in inject) {
        var val = inject[key];
        normalized[key] = isPlainObject(val)
          ? extend({ from: key }, val)
          : { from: val };
      }
    } else {
      warn(
        "Invalid value for option \"inject\": expected an Array or an Object, " +
        "but got " + (toRawType(inject)) + ".",
        vm
      );
    }
  }

  接下来是directives指令集选项的校验,我们来看看normalizeDirectives方法。

function normalizeDirectives (options) {
    var dirs = options.directives;
    if (dirs) {
      for (var key in dirs) {
        var def$$1 = dirs[key];
        if (typeof def$$1 === 'function') {
          dirs[key] = { bind: def$$1, update: def$$1 };
        }
      }
    }
  }

  这里主要规定了directives指令为自定义函数名的写法时,例如。

directives: {
    'color-swatch': function(el, binding) {
        el.style.backgroundColor = binding.value
    }
}

  这个时候函数的写法会将行为赋予bind,update钩子。关于directives的钩子,可以参考官方文档 https://cn.vuejs.org/v2/guide/custom-directive.html 

  我们回到mergeOptions函数。

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

   以上代码针对的是根据Vue构造器扩展的子类构造器的选项合并,关于子类构造器,在后面篇幅再阐述。接下来看mergeOptions方法剩下的部分。

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

  这部分代码阐述了Vue中基本的选项合并策略,一共可分为五类:

  1. 常规选项合并。

  2. 自带资源选项合并。

  3. 生命周期钩子合并。

  4. watch选项合并。

  5. props,methods, inject, computed类似选项合并。

  我们逐一分析,常规选项合并常见的有el,data的合并,首先是el合并,el提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标,因此它只在创建Vue实例才存在,在子类或者子组件中无法定义el选项,因此el的合并策略是在保证选项只存在于根的Vue实例的情形下使用默认策略进行合并。

    strats.el = strats.propsData = function (parent, child, vm, key) {
      if (!vm) {
        warn(
          "option \"" + key + "\" can only be used during instance " +
          'creation with the `new` keyword.'
        );
      }
      return defaultStrat(parent, child)
    };

 

  来看看data合并的代码。

strats.data = function (
    parentVal,
    childVal,
    vm
  ) {
    if (!vm) {
      if (childVal && typeof childVal !== 'function') {
        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)
  };

  vm代表Vue创建的实例,如果没有vm参数,则代表合并的两个对象为父子关系,并且当子组件的data类型必须为函数。来看看mergeDataOrFn方法。

/**
   * Data
   */
  function mergeDataOrFn (
    parentVal,
    childVal,
    vm
  ) {
  //如果是父子类关系时 if (!vm) { // in a Vue.extend merge, both should be functions

    //如果子类没有data选项,则返回父类的data选项 if (!childVal) { return parentVal }
    //如果父类没有data选项,则返回子类的data选项 if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) } } else { return function mergedInstanceDataFn () { // instance merge var instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal; if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } }

 

  如果父类实例和子类实例都有data选项时,mergeDataOrFn方法会返回一个mergedDataFn方法,该方法返回一个mergeData函数,参数分别为子类,父类实例执行data函数返回的对象。该函数也是最终options选项中data选项的值,来看看mergeData方法的实现。

function mergeData (to, from) {
if (!from) { return to } var key, toVal, fromVal;    //在浏览器支持Symbol类型时返回所有属性Key,包括不可枚举的属性,不支持Symbol类型时则返回不包括不可枚举属性的key var keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); for (var i = 0; i < keys.length; i++) { key = keys[i]; // in case the object is already observed...
    
    //如果key为响应式key,则该key值不参与合并, if (key === '__ob__') { continue } toVal = to[key]; fromVal = from[key]; if (!hasOwn(to, key)) { set(to, key, fromVal); } else if ( toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal) ) { mergeData(toVal, fromVal); } } return to }

  此时to参数为子类实例data函数执行后返回的对象,from参数为父类实例data函数执行后返回的对象,如果没有from对象,则返回to对象,如果在子类data对象中找不到父类data对象的key值时,则会将父类data对象的key对应的value值赋予子类data对象,也就是说,data选项的合并策略就是父类data里的属性会合并到子类data里,如果父类子类属性冲突,则保留子类的属性,如果父子类的key对应的value也是一个对象,既为深层嵌套,则将递归执行mergeData方法进行合并。最终返回一个经过层层合并后的子类实例data对象。

  我们回到mergeDataOrFn方法,如果vm存在,既Vue实例化时,会执行mergedInstanceDataFn方法,该方法实现和上述思路大同小异,这里不再赘述。

  接下来是自带资源选项的合并,我们来看看Vue自带的资源选项有哪些。

// 资源选项
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);
      return extend(res, childVal)
    } else {
      return res
    }
  }

  mergeAssets方法的逻辑也比较简单,如果有子类选项,则合并到父类选项里去,如果子类选项不存在,则返回父类的选项。

  接下来是生命周期钩子函数的合并。

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
  ) {
    // 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
}

  主要的合并过程都在代码注释里面了,接下来是watch选项的合并。

  strats.watch = function (
    parentVal,
    childVal,
    vm,
    key
  ) {
    // work around Firefox's Object.prototype.watch...

//火狐浏览器在Object的原型上拥有watch方法,这里对这一现象做了兼容
    if (parentVal === nativeWatch) { parentVal = undefined; }
    if (childVal === nativeWatch) { childVal = undefined; }
    /* istanbul ignore if */
  
  //没有子类,则返回父类 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 };

  对于watch选项的合并,最终父类选项和子类选项合并为数组,并且数组的选项可以为函数,函数名,选项对象,接下来是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
  };

   源码设置将这几个选项归合并策略为一类,如果父类选项不存在,则返回子类选项,并且子类选项类型保证为对象,如果父类子类选项都存在,则用子类选项去覆盖父类的选项。

  写到这里,我们回到一开始执行的_init()函数,此时函数代码只执行到选项合并环节,这是我们本篇文章暂时记录到的点,下一篇文章将阐述一下Vue的数据代理。

上一篇:Vue2.X源码学习笔记(二)数据代理


下一篇:简单对比vue2.x与vue3.x响应式及新功能