vue中mergeOptions的合并策略

vue实例中$options是一个很关键的属性,组件在渲染的过程中需要查找的component、filter等都从这里查找。而$options的内容大多是通过mergeOptions这个方法得到的。mergeOptions主要的调用地方大概有以下几处

1、vue初始化函数_init中,将,所有的vue组件渲染都会走_init方法

Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid++;
    // 其他代码省略……
    // merge options
    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 {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    // 其他代码省略……
}

这里的mergeOptions函数大概干了哪些事情?对什么进行了合并,这里举个简单的例子。

const options = {
  data: {},
  methods: {}
}
new Vue(options)

针对上面这段代码,_initmergeOptions合并的是Vue.options和自己定义的options。具体两个对象如何合并,比如两个options中都有data,都有methods,那合并后的options对象是什么样的?这里就涉及到本文说的一个合并策略的问题了,后面再具体研究。

那么Vue.options里面是什么呢?在哪里初始化的?

这里的Vue就是构造函数,其初始化可以从源码中*initGlobalAPI*方法里查看

function initGlobalAPI (Vue) {
  // 其他代码省略……
  
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive
  };

  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;

  Vue.options = Object.create(null);
  //ASSET_TYPES 指 ['component','directive','filter']
  ASSET_TYPES.forEach(function (type) {
    Vue.options[type + 's'] = Object.create(null);
  });

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue;
	//全局组件 builtInComponents  keepAlive
  extend(Vue.options.components, builtInComponents);

  initUse(Vue);	//Vue.use方法初始化
  initMixin$1(Vue);	//Vue.mixin方法初始化
  initExtend(Vue);	//Vue.extend方法初始化
  initAssetRegisters(Vue);	//Vue.component等全局组件注册函数初始化
}

根据这一步,至少可以知道Vue.options最初包含哪些属性

{
  components: {},
  directives: {},
  filters: {},
  _base: Vue
}

注意:构造函数options中的components中存放的是全局组件。全局注册函数Vue.component(其他两个directive,filter同component)注册的组件存储在构造函数的options里(Vue.options),具体想了解的这块可以参考initAssetRegisters方法。

此外,如果引用了vue相关的插件,例如vuex、vue-router等,这些插件在安装的时候会通过mixin混入beforeCreate钩子函数的方式进行注册,同时也可能混入一些其他的例如destroyed等.

所以,引入了vuex的Vue.options中还包含钩子函数

{
  components: {},
  directives: {},
  filters: {},
  _base: Vue,
  beforeCreate: [vuexInit],
  destroyed: [fn]
}

这里需要特别提出的是initGlobalAPI中最后面调用的initMixin$1initExtend方法内部都调用了mergeOptions方法

2、在resolveConstructorOptions方法中调用

function resolveConstructorOptions (Ctor) {
  var options = Ctor.options;
  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
}

3、在Vue.mixin中调用,该方法在前面提到过的initGlobalAPI中调用

function initMixin$1 (Vue) {
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin);
    return this
  };
}

4、在initExtend中调用,该方法在前面提到过的initGlobalAPI中调用

Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

		// 此处省略其他代码……
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
  	//将父类构造函数options和子类做合并
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    Sub['super'] = Super;

    // 此处省略其他代码……
  };
}

这里值得一提的是,当执行Vue.component时,其内部会执行Vue.extend方法。这里提一个思考,为什么Vue.component可以注册全局组件,而其内部调用的Vue.extend却不能注册全局组件?

合并策略

首先,来看下mergeOptions的具体内容,看看都干了些什么

function mergeOptions (
  parent,
  child,
  vm
) {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child);
  }

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

  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirectives(child);
  var extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  // 这一步与后面循环mergeField的执行顺序很关键,决定了是mixin中覆盖当前option还是当前option覆盖mixin
  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
}

这个函数内容并不是很长,但是通过前面的介绍,也可以看出这个函数在整个vue的执行过程中很重要。

该方法首先执行了几个normalize方法,这几个方法主要的作用是将options中某些属性进行统一格式管理。

首先是normalizeProps方法,该方法将props属性统一格式化成一个对象,其格式如下

{
  prop1Name: {type: String}
}

常见的props写法有两种,一种是数组形式,一种是对象形式

{
  props: ['prop-name1','propName2']
}
//经过normalizeProps格式化后,将变为 {propName1: {type:null},propName2: {type:null}}

或者

{
  props: {
    propName1: {type: String},
    propName2: {type: Number}
  }
}

normalizeInject顾名思义,是用来格式化inject格式的。常见的inject的写法也有两种:

{
  inject: ['message']
}
{
  inject: {
    localMessage: {from: 'message'}
  }
}

格式化后的格式统一按照第二种对象格式来

normalizeDirectives用来格式化指令,指令支持两种格式,一种是直接定义函数,一种是定义指令对象

{
  focus: function(el,binding,vnode) {}
}
//格式化后为 { focus: { bind: function(el,binding,vnode) {}, update: function(el,binding,vnode) {}}}
{
  focus: {
    bind: function(el,binding,vnode) {},
    update: function() {}
  }
}

normalizeDirectives是将第一种格式格式化成第二种格式。

接下来,继续看mergeOptions中紧接着递归执行了merge操作。

function mergeOptions (
  parent,
  child,
  vm
) {
    //此处省略其他代码
    var extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  // 这一步与后面循环mergeField的执行顺序很关键,决定了是mixin中覆盖当前option还是当前option覆盖mixin
  if (child.mixins) {
    for (var i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
  }
    //此处省略其他代码
  }

这里有两种,如果options里面有extends属性或者mixins属性,会递归合并。从上面的源码中可以看出,下面两种书写的形式是等价的

{
  data: {},
  extends: mixin1
}

//等价于
{
  data: {},
  mixins: [mixin1]
}

最后面就是真正的合并相关代码了

function mergeOptions (
  parent,
  child,
  vm
) {
    //此处省略其他代码
    var options = {};
    var key;
    for (key in parent) {
      //parent中所有key值属性和child的key值属性进行合并
      mergeField(key);
    }
    for (key in child) {
      if (!hasOwn(parent, key)) {
        //parent中没有,child中有的
        mergeField(key);
      }
    }
    function mergeField (key) {
      //strats中定义了一些关键key的合并策略,根据key取对应的合并策略,进行合并。
      var strat = strats[key] || defaultStrat;
      options[key] = strat(parent[key], child[key], vm, key);
    }
   return options
}

看着就这么点代码,是不是感觉这个源码读起来就没那么费劲了。简单直白的说这个函数的最终目的是合并两个options对象,返回合并后的options。

默认合并策略defaultStrat

首先看下源码中的默认合并策略源码,这段源码内容很简单,如果childVal有值,取childVal,否则取parentVal。是一个覆盖的合并策略,如果parent和children同时拥有该属性,也返回child。

var defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
};

源码中有段代码定义了elpropsData使用该合并策略

strats.el = strats.propsData = function (parent, child, vm, key) {
    return defaultStrat(parent, child)
};

也即是说如果有以下两个options

const option1 = {
  el: '#app'
}
const option2 = {
  el: '#main'
}
//mergeOptions(option1,option2)后
const options = {el: '#main'}
data合并策略
strats.data = function (
  parentVal,
  childVal,
  vm
) {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      return parentVal
    }
    return mergeDataOrFn.call(this, parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm)
};

大家都晓得,data的写法有两种形式,一种是plain object,一种是函数。在做合并的时候,会对类型进行判断,如果是函数,将函数执行的结果作为合并的对象进行合并。具体的合并可以参考mergeData方法

function mergeData (to, from) {
  if (!from) { return to }
  var key, toVal, fromVal;
  var keys = 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 (isPlainObject(toVal) && isPlainObject(fromVal)) {
      mergeData(toVal, fromVal);
    }
  }
  return to
}

从上面的代码中可以看出,这是带有一个递归合并的函数,当data中某一属性值为plainObject时,会进行递归合并。下面以实际的例子来具体说明究竟是怎么合并的

const option1 = {
  data: {
    a: 1,
    b: 2,
    o: {
      c: 3
    }
  }
}

const option2 = {
  data() {
    return {
      a: 'a',
      o: {
        c: 'c',
        d: 'd'
      }
    }
  }
}

假设有以上两个option,在执行mergeOptions(option1,option2)时,首先执行option2`中的data函数,得到对应的执行结果对象

{
  a: 'a',
  o: {
    c: 'c',
    d: 'd'
  }
}

然后就是将该对象与option1进行合并,执行mergeData方法。

首先遍历option1的key值,如果有option1中有,option2中没有的属性,则执行set(to, key, fromVal)操作,两者都有的属性,取option2,因为最后返回的是to,遍历过程中的操作是对to对象进行的操作,而不是返回一个新对象。这点要注意。所以option1.a和option2.a合并后,返回的是option2对象,自然a的取值是’a’。

接着遇到属性o时,这是一个plain object。会递归执行mergeData操作,即执行mergeData(option2.o,option1.o)

最终合并后,会得到以下的结果

{
  a: 'a',//相同属性取option2
  b: 2, //option2没有的属性,新增
  o: {
    c: 'c',
    d: 'd'
  }
}
钩子函数的合并策略
function mergeHook (
  parentVal,
  childVal
) {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}
//LIFECYCLE_HOOKS钩子函数
LIFECYCLE_HOOKS.forEach(function (hook) {
  strats[hook] = mergeHook;
});

钩子函数有哪些,相信大家应该不陌生。这里还是简单列举下,便于对照上面的代码

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
];

从上面的代码中可以看出,这些钩子函数的合并策略是一样的。具体如何执行合并呢?无外乎那么几种情况,a有b没有,a没有b有,和a、b都有。这里也以具体的对象进行举例。

const opt1 = {
  beforeCreate() {console.log('opt1 beforeCreate')},
  mounted() {console.log('opt1 mounted')}
}
const opt2 = {
  created() {console.log('opt2 created')},
  mounted() {console.log('opt2 mounted')}
}

当执行beforeCreate的合并时,就出现了a有b没有的情况,即opt1中有beforeCreate而opt2中没有。这种合并后的beforeCreate就取opt1中的。

当执行created合并时,就出现了a没有b有的情况,合并后取opt2的created

前面两种很符合我们日常的合并思维,很容易理解记忆。我们应当注意的是第三种,a有b也有的情况。当我们合并mountd的时候,合并后的mounted是什么?根据源码执行可知道,会将该属性值转换成数组,进行追加。及合并后的mounted是个数组,数组的内容为[opt1.mounted, opt2.mounted]

最终mergeOptions(opt1,opt2)后会得出以下结果

{
  beforeCreate() {console.log('opt1 beforeCreate')},
  created() {console.log('opt2 created')},
  //这里注意数组里方法的顺序,决定了合并后,执行的属性
  mounted: [function() {console.log('opt1 mounted')},function(){console.log('opt2 mounted')}]
}

合并后,所有的钩子函数都会重新执行。注意执行顺序

Assets合并策略
function mergeAssets (
  parentVal,
  childVal,
  vm,
  key
) {
  // [components, directives,filters]通过原型链接的方式进行合并
  var res = Object.create(parentVal || null);
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm);
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets;
});

Vue中的Assets主要指以下3种属性

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

如源码中注释的那样,两个options中assets属性合并,主要是通过原型继承的方式,最终返回一个新的对象,该对象包含childVal,并且原型指向parentVal。举个简单的例子

const opt1 = {
  components: {ComponetA}
}
const opt2 = {
  components: {ComponentB}
}

执行mergeOptions(opt1,opt2)后,其options中component的属性如下,这样合并后通过components依然能查找到opt1中的ComponentA

{
  components: {
    ComponentB
    [[prototype]]: {ComponentA}
  }
}

一般情况下,以new Vue({components: {ComponentA}})来说,会执行mergeOptions(Vue.options, {components: {ComponentA}})`。合并后的components里ownProperty是局部组件,原型(components.proto)中是全局组件。

watch合并策略
strats.watch = function (
  parentVal,
  childVal,
  vm,
  key
) {
  // work around Firefox's Object.prototype.watch... firefox58+版本已经移除
  if (parentVal === nativeWatch) { parentVal = undefined; }
  if (childVal === nativeWatch) { childVal = undefined; }
  /* istanbul ignore if */
  // childVal没值,返回空对象 原型继承parent watch
  if (!childVal) { return Object.create(parentVal || null) }
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm);
  }
  //parentVal无值,直接返回childval
  if (!parentVal) { return childVal }
  // 合并parent child,相同key值,value转换成数组合并
  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、opt1有watch属性,opt2没有,直接返回`Object.create(opt1.watch)

const opt1 = {
  watch: {message() {}}
}
const opt2 = {}
//mergeOptions(opt1,opt2) 合并后
const opt = mergeOptions(opt1,opt2) = { 
  watch: {
    [[prototype]]: {message() {}}
  }
}

2、opt1没watch属性,opt2有,合并后的watch值,为opt2.watch

const opt1 = {}
const opt2 = {
  watch: {message() {}}
}
//mergeOptions(opt1,opt2) 合并后
const opt = mergeOptions(opt1,opt2) = { 
  watch: {message() {}}
}

3、opt1和opt2都有watch属性

const opt1 = {
  watch: {
    message() {}
  }
}
const opt2 = {
  watch: {
    message() {}
    other() {}
  }
}
//mergeOptions(opt1,opt2) 合并后
const opt = {
  watch: {
    message: [Function,Function],//opt1.watch.message  opt2.watch.message
    other: [Function]//opt2.watch.other
  }
}
props,methods,inject,computed的合并策略
strats.props =
  strats.methods =
  strats.inject =
  strats.computed = function (
    parentVal,
    childVal,
    vm,
    key
  ) {
    if (childVal && process.env.NODE_ENV !== 'production') {
      assertObjectType(key, childVal, vm);
    }
   // 无parentVal,返回childVal
    if (!parentVal) { return childVal }
  //注意,这里原型指向null
    var ret = Object.create(null);
  //拷贝parentVal属性
    extend(ret, parentVal);
    // 对象合并,同名属性,child覆盖parent
    // 思考,{ mixins: [mixin1,mixin2], methods: {fun1() {}} }
    // mixin1中也有fun1的定义,合并后的method中是哪个fun1
    // 解:在mergeOptions中先mergeOptions了parent和child.mixin1,在后面才处理child自身。
    if (childVal) { extend(ret, childVal); }
    return ret
  };

依旧有三种情况,这里不具体一一展开了。需要留意的是第二种当child无值时,不是直接返回parent,也非原型链接。而是将parent属性拷贝到一个新对象。

这里重点关注下parent和child都有值时的场景,两者都有值时,child中对应属性的值覆盖parent属性的值。举例说明

const opt1 = {
  methods: {
    fun1() { console.log('opt1')}
    fun3() {}
  }
}

const opt2 = {
  methods: {
    fun1() { console.log('opt2')},
    fun2() {}
  }
}

//mergeOptions(opt1,opt2) 合并后
const opt = {
  methods: {
    fun1() { console.log('opt2')},
    fun2() {}
    fun3() {}
  }
}
provide的合并策略
strats.provide = mergeDataOrFn;

与data的合并策略一样,参考data吧。

以上就是源码中定义的一些option关键属性的合并策略,理解这些合并策略,可以帮助你更好的理解Vue的执行机制。

你可能感兴趣的:(vue.js,javascript,前端)