Vue学习(一):Vue中的选项合并策略

开端

最近为了给自己充电,在腾讯课堂报了一个vue源码学习的课程,跟着课堂上的老师学习vue源码以此来增加自己对于vue的理解。这篇文章主要是记录一下自己学习vue源码时,学习到了什么,并把自己对vue源码的理解通过语音的形式给描述出来。这里并不会一字一句的解释vue中的每一句源码的意思,因为有些我自己还不解其意,但不妨碍从中学习到知识。

vue版本:2.5.1
源码范围

1.实例化vue时参数的合并策略以及规范。
2.vue通过Vue.extend方法进行组件参数扩展时的参数合并策略。
3.vue的mixins字段的参数合并方式。


使用vue创建一个实例时,传递给Vue的构造函数的参数会变成什么?

var vm = new Vue({
    el: '#app',
    data: {
        message: 'hello world!',
    },
});

这里我们不探究new Vue时,其他地方发生了什么,本篇文章我们只关心vue源码中是怎么处理Vue构造函数接收过来的参数的。我们不妨通过控制台打印一下 vm.$options 看一下是什么:
 

vm.$options对象的输出结果
vm.$options对象的输出结果标题

这$options属性就是vue实例存储的合并Vue参数后的对象,有el,components组件,data数据,directives指令等。首先我们上面的例子中并没有传递components和directives这两个参数,可实例中却有值,然后data却是一个函数,这又是为什么呢?vue中他究竟是怎么合并这些参数的呢?下面我们就从简单到复杂慢慢来学习一下vue的源码中究竟做了什么事吧。

Vue的初始化函数 —— _init函数

首先,Vue对象是一个构造函数,我们先隐匿掉其他部分,只留下和参数合并相关的代码,以便让我们进行关注点分离,更容易学习,精简后的Vue构造函数长这样:

function Vue (options) {
    this._init(options);
}

哦豁,真的是简单的不能再简单了,其实也确实如此,_init函数就是Vue实例化的入口,我们来找到_init函数,看他又做了什么:

Vue.prototype._init = function (options) {
  var vm = this;
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  );
};

上面的代码看起来只有一个作用,就是通过mergeOptions函数合并vm实例的参数,当然整个_init方法远不止这么简单,但我们的目的很明确:学习vue中的对于参数的合并处理是怎么做的,其他vue中对于一些其他操作我们可以忽略,事实上我也确实是把_init方法给删的只有一个功能了。你此时只需要关系vue的参数合并功能即可。这也是这篇文章的目的。

mergeOptions方法

重点在于这个mergeOptions方法,他的作用就是合并用户传递的参数的,看这一句:

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

我们给mergeOptions方法传递了三个参数,第二个第三个好理解,一个就是用户传递参数,没传则为{}对象,vm就是当前new Vue时创建的那个实例,而这一句是什么? resolveConstructorOptions(vm.constructor) ,我们知道Vue中是有默认参数的,因为上一个例子中我们并没有向Vue构造函数中传递components参数,可vm实例中却还是有一个components属性,说明这是vue给他的默认值,那vue的默认值在哪呢?由于此处vue中的源码不是那么简单就可以拿出来的,所以省略为如下代码:

Vue.options = {
    components: {},
    directives: {},
    _base: Vue,
};

也就是说vue的默认参数是创建在Vue这个构造函数中的,当然还有其他的,只是简化成这样,实际要复杂的多,不过正如我之前所说的:我们的关注点不在这里,则隐藏它对于我们来说就很有必要了。
那么 resolveConstructorOptions(vm.constructor) 其实就是合并构造函数中的默认参数的,此处你可以直接变成这样:

mergeOptions(
    vm.constructor,
    options || {},
    vm
)

那么mergeOptions的作用就很明显了,合并Vue中的默认参数和用户传递过来的参数。

ps:为什么 resolveConstructorOptions 方法可以忽略?好吧,因为那个课堂老师没讲,然后我自己去看了一下也没看太懂,应该是合并父子类构造函数的默认参数的。没事,问题不大。

那mergeOptions方法内部究竟做了什么呢?我们来看一下:

  function mergeOptions (parent, child, vm) {

    checkComponents(child);

    normalizeProps(child, vm);
    normalizeInject(child, vm);
    normalizeDirectives(child);

    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
  }

这个mergeOptions做的事不少,大致有以下几件:

1.checkComponents函数,校验child对象中组件参数components中的组件名是否符合规范
2.normalizeXXX函数,用来统一参数的形式,比如props支持数组和对象的写法,normalizeXXX函数的作用就是将他们统一转换为同一种形式,以便后续使用。
3.extends和mixins混入对象和合并,如果他们有extends或者mixins字段,则先进行遍历递归合并他们。
4.将child对象,扩展到parent对象上。

这里面,child和parent都是参数对象,而且,child的优先级要搞,也就是如果出现相同的属性参数,则以child中的为准,记住这个设定。后面的其他函数的设置都是后面的参数覆盖前面参数。

那么我们就是来讲讲他们的作用吧。

normalizeXXX函数

首先,checkComponents这个函数就不用讲了,校验组件名是否合法的,各位自行查看源码即可。现在看来一下类似的normalizeXXX函数做了什么事吧,我们拿normalizeDirectives做参考即可,这是代表指令的参数:

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

代码不多,首先我们都知道,我们注册局部时,directives参数是一个对象,里面每一个指令属性值既可以接受一个函数,也可以指定一个对象用于设置指令的钩子函数。如果指令属性的值是一个函数,则它是同时作为bind和updata钩子函数进行绑定的,那么这时候,你就需要处理这两种不同的写法,将其转换为同一种写法了:
他会遍历options.directives,然后他会判断这个对象上面的每一个值,如果是函数,则执行:

dirs[key] = { bind: def, update: def };
也就是统一把他转换为一个成对象的形式,如果本身是对象的,则不需要处理。
例子@code
var vm = new Vue({
    el: '#app',
    directives: {
        dir1: function dir1() {

        },
        dir2: {
            bind() {

            },
            updata() {

            },
        },
    },
})
合并指令后的参数结果
合并指令后的参数结果


很简单的一个处理方式,其实就是统一一下参数形式,以方便后续的使用。normalizeProps函数也是类似,因为组件的props参数可以接收一个数组,或者是一个对象,所以normalizeProps函数也是把props中的数组转换成对象的形式。

extends和mixins合并

extends和mixins呢,其实理解起来也比较容易,首先如果extends和mixins存在的话,则通过递归调用mergeOptions方法将其合并到parent上去,至于为什么是合并到parent上去而不是child或者一个新的空对象,是因为child合并的优先级最高,不能先合并,而与其合并到一个新对象上去,然后再和panent合并,何不索性直接合并到parent上,然后再通过child覆盖到parent上,这样简便了很多。

参数合并

最重要的是这一个地方,真正的参数的合并,看一下代码

var options = {};
var key;
// 遍历parent中的属性
for (key in parent) {
  mergeField(key);
}
// 遍历child中的属性
for (key in child) {
  // hasOwn的作用在于,检测parent中是否存在这个key,如果存在,则跳过,不会重复执行mergeField方法。
  if (!hasOwn(parent, key)) {
    mergeField(key);
  }
}
// 合并参数的函数
function mergeField (key) {
  var strat = strats[key] || defaultStrat;
  options[key] = strat(parent[key], child[key], vm, key);
}

上面注释很明白了,遍历panent和child的所有key,调用mergeField方法进行合并,那mergeField做了什么呢?他的目的是获取参数合并函数,随后执行得到合并后的结果。那么参数合并函数是怎么来的呢?那这里就要说一下参数的合并策略了。不过在说合并策略之前,因为vue的自定义合并策略中,它除了处理了用户在通过new来创建一个vue实例这种情况的合并之外,还处理了用于使用Vue.extend这个方法进行参数扩展这种情况的合并,所以,在这里先讲一个Vue.extend这个方法对于vue的自定义参数合并策略有什么什么影响吧:

Vue.extend方法

Vue.extend这个函数有什么功能,我这里就不详细介绍了,简单说一下即可:

const Sub = Vue.extend({
    data() {
        return {
            message: 'hello world',
        }
    }
})

上面这段代码中,我们通过Vue.extend生成了一个Vue这个构造函数的子类,对js原型有所了解的应该知道啊,那么,他的作用是什么呢?我们先不管他的其他作用,也不关心他继承时,内部做了哪些其他操作,我们只关心这个Vue.extend扩展子类时,他对于这个子类在使用时的参数合并有什么影响即可。这是我们的一贯作风。

好,我们知道,Vue.extend在扩展了一个子类时,传递的参数和我们使用组件时,传递的参数一样,而且,我们通过Sub这个子类构造函数创建vue实例时,即使我们不为data设置值,我们也会拥有一个message这个data属性值:

const vm = new Sub({});
console.log(vm.message);  // 'hello world'

那么vue中是如何做到的呢?我们知道,Vue这个构造函数中,有一个Vue.options这个属性,这个属性我上文提到说是我们在new一个Vue的实例时,参数合并的默认参数,默认的Vue.options为:
 

// 极度隐藏细节的简写,但Vue这个构造函数中确实有这个options属性,你们可以尝试输出一下,
// 只不过components和directives这些肯定是不为空对象就是了。
Vue.options = {
    components: { ... },
    directives: { ... },
    _base: Vue,
};

那么我们来尝试打印一下Sub.options,看他这个子类构造函数是否拥有这属性呢:
 

Sub.options的打印结果


我们发现,Sub.options中有值,而且还不少,其中我们发现data为一个函数,到这里你是不是了解了Vue.extend中对于参数合并做了什么了吗?如果还不太明白我们直接看一下源码吧:
 

Vue.extend = function (extendOptions) {
  extendOptions = extendOptions || {};
  // 这里的Super指的是Vue构造函数(调用Vue.extend()方法的情况下)
  var Super = this;

  // 其他代码

  // Sub构造函数
  var Sub = function VueComponent (options) {
    this._init(options);
  };
  // 为Sub构造函数做原型继承。
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  // 合并参数
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  );

  // 其他代码

  Sub.extend = Super.extend;

  // 其他代码

  return Sub;
};

这是删减后的Vue.extend方法,我们看到返回的Sub构造函数,其他的原型继承就不说了,那么看这一段:

Sub.options = mergeOptions(
  Super.options,
  extendOptions
);

这一段是什么呢?我们以上面的使用Vue.extend扩展一个叫Sub的子类构造函数这个例子来分析一下:

1.当调用Vue.extend时,Super.options指得就是Vue.options,也就是Vue构造函数上的默认参数属性
2.而extendOptions这个参数指的是你使用Vue.extend这个方法时,给他传递的那个参数
3.然后调用mergeOptions合并Vue.options和extendOptions这两个对象并赋值给Sub这个构造函数的options属性(即:Sub.options)

哦,这就是很明了了,其实Vue.extend方法对于参数的影响就是使用mergeOptions函数将父类的默认参数和你传入的参数进行合并,并赋值给这个Sub子类构造函数的option属性,说白了,就是重新给扩展的这个Sub子类重新设置默认参数的。然后,我们在new Sub时,通过_init这个初始化方法再合并参数时,那么合并的默认参数中就会带有data属性了。所以,后续所讲的一些参数合并策略中,会对一个叫做vm的参数进行判断,就是为了区分,这次参数合并是new一个vue实例呢还是在使用extend方法进行子类构造函数扩展呢。好,下面就开始真正讲具体的参数和合并策略了。


合并策略

什么是参数的合并策略?我们知道不同的参数合并起来可能不一样,比方说,大多数的参数合并起来可能直接是覆盖就行了,比如说js中的原始值的合并(string,number这些)。而有些参数,可就不是直接覆盖那么简单了,比如vue中的钩子函数,你在使用mixins时可能定义了多个相同的created这个钩子函数,那么我们能直接简单的覆盖吗?No,他会合并成一个数组,然后在正确的时机依次调用。

由此我们看出,不同的参数很可能有不同的合并策略,那么我们就需要为不同的参数指定不同的合并策略函数,以此来进行合并。那么这个strats就是存储不同参数合并策略函数的对象。
大致长这样:

var strats = Object.create(null);
strates.data = function () { ... };
strates.created = function () { ... };
strates.watch = function () { ... };

也就是如果vue中有需要自定义合并策略函数的参数,vue中就很往strats对象上扩展一个方法,然后mergeField函数中会判断:如果strates对象上有属性的合并策略函数,则使用strates上的合并策略函数,否则,使用defaultStrat这个默认的策略合并函数。(这个defaultStrat函数就是大多数情况下的直接覆盖的那个默认策略函数)

理解了这个,那么我们就知道,如果是普通的属性,比如一个name,那么直接使用默认策略进行覆盖即可,而如果是拥有特殊合并策略的属性,则会调用strates上定义的特殊策略函数进行合并,那么我么最主要的关系的就是究竟一些特殊合并方式在vue是怎么实现的,那么我们就一个一个的来看吧。


开始:合并策略中额外的使用的工具函数

  /**
   * Mix properties into target object.
   将一个对象上的属性值扩展到另一个对象上
   */
  function extend (to, _from) {
    for (var key in _from) {
      to[key] = _from[key];
    }
    return to
  }

el选项

看一下el属性在vue中是如何合并的:

strats.el = function (parent, child, vm, key) {
  if (!vm) {
    // 提示错误。el参数选项只能在vue实例中传入
  }
  return defaultStrat(parent, child)
};

在mergeField函数调用合并策略时,会向其传递四个参数:第一第二个参数就不说了,是需要合并参数的值,而vm和key中,vm指vue的实例,如果是通过new来创建一个vue的构造函数时,vm会有值,而通过Vue.extend方法进行组件扩展时,则vm为空。

所以el这个合并策略很简单,只是判断了一下他是不是vue的实例,如果不是vue的实例的话,则不能使用el这个参数选项。

data

我们来看一下vue源码中对于data属性选项中的值,是怎么进行合并的吧。倒是很复杂,因为data属性在用户通过new来创建一个vue实例时,data可以是函数也可以是对象,而在组件及Vue.extend方法中,data选项必须是一个函数。那么我们来看一下他们在合并后是什么样子的吧。

strats.data = function (
  parentVal,
  childVal,
  vm
) {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      // 如果child存在且不是一个函数,则报错;
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
};

这里我们看到,其实这里只做了一判断,我们知道vm指的是vue的实例,其实这里只是做了一个判断:如果是不是通过一个new实例来进行data的合并时,childVal存在且不是一个函数,那么就会提示一个报错信息,并直接返回parentVal,什么情况会不是创建一个vue实例而需要合并参数呢?比如通过Vue.extend方法来扩展一个Vue的子类时,所以,这里需要进行一个判断。

然后我们看到返回一个mergeDataOrFn函数的执行结果。那么这个mergeDataOrFn函数是什么呢?我们来看一下:

function mergeDataOrFn (
  parentVal,
  childVal,
  vm
) {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    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
      }
    }
  }
}

我们一个一个来分析吧,可能有点绕,首先这一句的判断:

if (!childVal) {
  return parentVal
}
if (!parentVal) {
  return childVal
}

如果childVal没有则返回parentVal,然后childVal存在,如果parentVal没有,则返回childVal。没什么好说的。然后接下来会根据vm判断是创建vue实例的情况还是一些比如通过Vue.extend扩展时的情况进行分别处理,我们先看通过Vue.extend时,data合并的情况,也就是 !vm 为true的情况:

return function mergedDataFn () {
  return mergeData(
    typeof childVal === 'function' ? childVal.call(this, this) : childVal,
    typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
  )
}

他返回一个函数,其实mergeDataOrFn这个函数返回的都是一个函数,我们通过控制台分别打印一下以下代码看一下合并后的data是否是一个函数:

const Sub = Vue.extend({
    data() {
        return {
            message: 'hello world',
        }
    }
})
const Sub2 = Sub.extend({
    data() {
        return {
            message: 'this is Sub',
        }
    }
});
// Sub2.options指的是这个子类构造函数的默认options参数,这和Vue构造函数上options属性(Vue.options)是同一性质的。
console.log(Sub2.options);
const vm = new Sub2({
    data: {
        message: 'hello world',
    }
});
console.log(vm.$options);
data合并的结果图片
data合并的结果图片

我们看到,合并后的data属性确实是一个函数,而且Sub2这个构造函数是通过extend方法进行扩展后的,而这个构造函数的默认参数options中的data是一个函数,而vm这个Sub2这个实例,他的data合并后也一个函数。

好了,我们来回到上面的代码中来,先看第一个判断:

// in a Vue.extend merge, both should be functions
if (!childVal) {
  return parentVal
}
if (!parentVal) {
  return childVal
}
return function mergedDataFn () {
  return mergeData(
    typeof childVal === 'function' ? childVal.call(this, this) : childVal,
    typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
  )
}

在这里,前面那两个if就不用说了,我们来看那个return,他返回一个函数,这个mergedDataFn和我们上面打印出来的第一个结果中的data的函数名一样的,那就没错,这个函数做了什么呢?就返回了调用了一个mergeData这个函数的结果,而里面的三元运算表达式的作用其实就是获取需要parentVal的值和childVal的值,因为parentVal和childVal是一个函数,它会返回我们需要的data值,然后mergeData函数的作用就是合并两个data对象的。这个三元运算表达式是一个保险。

然后我们看一下另一种情况

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

这里面也是返回一个函数,这个函数中instanceData和defaultData也是为了拿到需要合并的data的值,然后做了一个判断,如果instanceData没有值,则直接返回defaultData,否则就要调用mergeData方法,将两个data进行合并,并返回其结果,这样吧,在这里举个例子吧:

const Sub = Vue.extend({
    // data是一个具名函数
    data: function subData() {
        return {
            name: 'test',
            message: 'hello world',
        }
    }
})
console.log(Sub.options);
const vm = new Sub({
    data: {
        message: 'hello world',
    }
});
console.log(vm.$options);

我们拿上面那个分析一下:
在调用Vue.extend进行扩展时,需要进行一次合并(具体合并在上文的Vue.extend中提过),然后合并data时,执行到了mergeDataOrFn方法,此时: parentVal === undefined (因为Vue.options中没有data这个选项),然后因为是同Vue.extend进行的参数合并,所以,vm不存在,到这里直接返回childVal的值,即为subData函数(函数我命名了的)

然后在调用new Sub时,因为是通过了new进行了实例化,所以vm有值,然后此时:parentVal === subData函数  childVal ===   { message: 'hello world' }对象,这时候,mergeDataOrFn函数代码中的instanceData的值为subData函数执行后的返回值,是一个对象,childVal为一个对象,所以执行mergeData函数的作用就是接收两个对象并将它们进行合并。

ps:这里的strats.data合并函数返回的值永远是一个函数,而不是对象值,这个函数会在vue后面的响应系统阶段进行调用,从而获取数据,所以这里返回的只是一个函数,当然了,也有因为组件的data需要唯一这一层的考虑因素。

然后还有一个mergeData这个函数没讲,这个的话,其实就是单纯的一个将两个object对象进行递归合并的函数,长这样:

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)) {
      to[key] = fromVal;
    } else if (
      toVal !== fromVal &&
      isObject(toVal) &&
      isObject(fromVal)
    ) {
      mergeData(toVal, fromVal);
    }
  }
  return to
}

其实上面代码也比较好理解(有些地方和vue源码不一样,我改了一下,不过不要紧),to和from为两个对象时,则将这两个对象进行合并,主要是哪个for里面的代码,通俗来讲:

以to为目标,将from对象上的属性扩展到to对象上,所以遍历from对象,
如果to对象上面没有这个from对象中的key属性,则直接向to对象扩展这个key属性,值就是from对象上的值
如果to和from对象都有这个key且都是object对象(不是数组),则递归调用mergeData方法进行合并
否则,以to对象上的值为准,不需要做任何操作

以上就是vue中对于data属性进行的合并策略。主要是进行了data为函数值得校验,以及data的递归合并。不急,下面还有其他的选项合并,我们接着看。


watch

watch用来监听data数据的改变从而执行某些操作,watch是一个对象,其中的每一个属性都是一个函数。那么如果watch对象中有相同属性名的处理函数该如何合并呢?比如mixins中。我们知道watch合并后,如果触发了data数据变化,则watch多个处理函数是可以依次执行的,那么如何做到这样的合并呢?答案就是合并成数组,我么你来看源码:

strats.watch = function (
    parentVal,
    childVal,
    vm,
    key
  ) {

    /* istanbul ignore if */
    // 如果子元素为空,则使用原型。至于会不会因为原型链问题导致有一些原型链
    // 上的处理函数无法执行,这个无需担心,因为如果有合并的话,那每次
    // 都会合并到一个新的对象上,不会往拥有原型链的对象上合并。
    if (!childVal) { return Object.create(parentVal || null) }

    // 校验child必须为一个对象,否则报错

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

ps:此处parentVal和childVal是需要合并的watch对象。

首先他是进行了一番数据校验,比如校验childVal必须为一个对象,或者如果childVal为空则直接返回parentVal这些数据校验。然后创建一个ret对象,最后的结果会把parentVal和childVal中的所有属性都扩展到这个ret对象上,并返回他。
那么怎么进行扩展的呢?首先通过 extend(ret, parentVal); 将parentVal中的属性全部扩展到ret对象之中。然后遍历childVal中的所有属性也扩展到parentVal中。那么重点在于,如果ret经过 extend(ret, parentVal) 扩展后,已经存在了相同属性的处理函数,该如何合并?上面的合并代码包含以下几种情况:

  1. 如果ret中存在了和childVal对象中相同的值
    1. 如果他已经是一个数组,则直接向这个数组中添加childVal中的这个属性函数。
    2. 如果不是一个数组,则先将其变成一个数组,然后再添加childVal中的这个属性值。
  2. 如果ret中不存在遍历childVal时的属性,那么直接将其转换成数组然后给ret对象扩展这个属性。

基本上这个合并策略就是这个意思,就是遍历需要合并的对象,如果有相同的属性,则将其合并成一个数组。

生命周期钩子函数合并

在vue中,生命周期钩子函数也不会进行直接覆盖,也是可以多个相同的处理函数同时存在,依次执行,vue中的生命周期钩子函数如下:

    // 生命周期钩子函数
    var LIFECYCLE_HOOKS = [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated',
        'deactivated',
        'errorCaptured'
    ];

然后vue中通过遍历这个数组,向strats对象上一次添加合并策略函数:

  // 合并策略函数
  function mergeHook (
    parentVal,
    childVal
  ) {
    return childVal
      ? parentVal
        ? parentVal.concat(childVal)
        : Array.isArray(childVal)
          ? childVal
          : [childVal]
      : parentVal
  }

  LIFECYCLE_HOOKS.forEach(function (hook) {
    strats[hook] = mergeHook;
  });

这个也不难,其实这么多三元判断表达式,其实就是为了确保将多个相同的生命周期钩子合并为一个数组。其实拆开长这样:

   function mergeHook(parentVal, childVal, vm, key) {
        // 如果子元素没有值,那么直接返回父元素
        if (!childVal) {
            return parentVal;
        }

        if (parentVal) {
            return parentVal.concat(childVal);
        }
        else {
            if (Array.isArray(childVal)) {
                return childVal;
            }
            else {
                return [childVal];
            }
        }
    }

很明显不是吗,如果有多个生命周期钩子,则直接合并成数组并返回。

组件,指令和过滤器

和生命周期钩子函数类似:

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

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

  // 向strats上扩展合并策略处理函数。
  ASSET_TYPES.forEach(function (type) {
    strats[type + 's'] = mergeAssets;
  });

ps:parentVal和childVal在这里指的是默认的组件,指令,过滤器对象和用户传过来的组件,指令,过滤器对象。

mergeAssets又做了什么呢?他其实是通过原型链来进行合并参数,首先创建一个以parentVal对象为原型的对象,那么我们访问res的属性时,就可以通过原型链获取到parentVal上的属性,然后,如果childVal有值,那么通过extend方法,将childVal上的属性扩展到res上去,否则,直接返回res。那么,每一次通过Vue.extend方法扩展一个子类或者通过mixins扩展组件,指令,过滤器时,都通过原型链来进行合并。

ps:我们尝试来打印一下:new一个空的,不传递components参数时,合并后的参数时什么样的吧,看似其实合并后的components选项是一个空对象,其实不然:

空的components组件结果
空的components组件结果(打印vm.$options)

咦?发现了什么,没错,其实我们创建的vue实例中的合并后的components和directives参数看似是一个空对象,其实在他们的原型上面,拥有这vue内置的组件和指令在上面,这也就是为什么我们的vue实例可以在任何地方都使用到这些内置组件,以及你们可以尝试去使用Vue.component方法来扩展一个全局组件,然后创建一个vue实例,看一下这个vue实例中的components选项对象的原型链中,找不找得到你在全局中扩展的自定义组件。

其他:props,methods,inject,computed

这些属性的合并都是一样的,而且也比较简单,直接是覆盖的:

 strats.props =
  strats.methods =
  strats.inject =
  strats.computed = function (
    parentVal,
    childVal,
    vm,
    key
  ) {

    if (!parentVal) { return childVal }
    var ret = Object.create(null);
    extend(ret, parentVal);
    if (childVal) { extend(ret, childVal); }
    return ret
  };

先创建一个空的对象ret,然后将parentVal中的属性扩展到ret对象中,然后如果childVal也拥有值的话,则同样扩展到ret对象上,在此期间,如果parentval和childVal拥有同样的值,则会覆盖掉。

总结

以上大致就是vue中的选项合并机制,跟着课堂老师学习研究了许久,整理一下,觉得自己还是有很大的收获的。至于有什么收获呢,感觉最主要的就是对于选项的参数如何进行合并有了一次了解吧,当然,这只是vue源码中的冰山一角,其核心的部分完全没有涉及,比如响应式系统和组合系统这两个核心部分都没有涉及,这参数合并充其量只是vue中的一个初始化和准备环境吧,还有更远的路要走啊。

我们可以从vue的参数合并中源码中发现常见的参数合并的一些方式,注意:一切按实际需求

  1. 普通的js原始值(string,number,boolean等)直接后者覆盖前者即可。
  2. 对象的合并,如果合并双方都是对象,则使用递归的方式将其合并(数组好像是直接覆盖,并没有进行特殊合并)
  3. 对于函数,如果需要他们合并后都可以执行,则可以考虑将函数合并成数组,然后你可以将其包装成一个新函数,依次调用合并后数组中的函数。
  4. 如果合并的是一些特殊对象,不能递归合并,那么根据情况,你还可以使用原型链的方式进行合并

不过,学了点知识,总要用上,练练手,熟能生巧吗,因为笔者最近在做微信小程序开发的,我去找了一下,发现微信小程序中好像只有组件拥有一个叫做behaviors用于组件之间的代码复用的选项,对于小程序页面来说并没有类似的mixins这种代码复用机制,所以,刚好,把vue这一套参数合并机制用在微信小程序的Page构造器上,它是一个简单的用于合并微信小程序Page构造器参数的工具函数,用的就是vue的参数合并这一套,不过没vue那么多选项需要合并,只有data和生命周期钩子函数这两个需要合并,但支持多个mixins选项以及mixins嵌套合并,喜欢的可以去github上给个star,多谢。

ps:目前此工具只针对小程序的Page构造器进行参数合并,后续可能出一个通用的,可自定义合并策略函数以及可配置的工具出来吧。

工具函数介绍地址:https://blog.csdn.net/qq_33024515/article/details/87858053
github地址:https://github.com/nongcundeshifu/wxPageMixins

其他感悟-源码观看技巧

1.隐藏细节,关注点分离,隐藏我们不需要关注的代码
2.将源码根据功能分为几个部分,按功能或者其他进行拆分理解,一个一个来。
3.根据代码的结果来查看和理解代码的作用,而不是盲目理解和猜测:从结果来看作用,可以结合控制台输出来查看并理解其中源码每个阶段的执行步骤和结果。

猜你喜欢

转载自blog.csdn.net/qq_33024515/article/details/87857999