合并策略

在这一节合并策略中,我们主要分三个步骤来说明:配置合并的背景配置合并的场景以及合并策略

背景

我们可能会很好奇,为什么要进行配置合并?这是因为Vue内部存在一些默认的配置,在初始化的时候又允许我们提供一些自定义配置,这是为了在不同的场景下达到定制化个性需求的目的。纵观一些优秀的开源库、框架它们的设计理念几乎都是类似的。

我们举例来说明一下配置合并的背景:

Vue.mixin({
  created () {
    console.log('global created mixin')
  },
  mounted () {
    console.log('global mounted mixin')
  }
})

假设我们使用Vue.mixin方法全局混入了两个生命周期配置createdmounted,那么在我们的应用中,这两个生命周期配置都会反应到各个实例上去,无论是根实例还是各种组件实例。但对于根实例或者组件实例而言,它们也可能会拥有自己的createdmounted配置,如果不进行合理的配置合并,那么会出现一些意料之外的问题。

场景

要进行配置合并的场景不止一两处,我们主要介绍以下四种场景:

  • vue-loader:在之前我们提到过当我们使用.vue文件的形式进行开发的时候,由于.vue属于特殊的文件扩展,webpack无法原生识别,因此需要对应的loader去解析,它就是vue-loader。假如我们撰写以下HelloWorld.vue组件,然后在别的地方去引入它。
// HelloWorld.vue
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'hello, world'
    }
  }
}

// App.vue
import HelloWorld from '@/components/HelloWorld.vue'
export default {
  name: 'App',
  components: {
    HelloWorld
  }
}

因为我们在HelloWorld.vue文件中只提供了namedata两个配置选项,但真正调试的时候我们发现HelloWorld组件的实例上多了很多额外的属性,这是因为vue-loader帮我们默认添加的。

const HelloWorld = {
  beforeCreate: [function () {}],
  beforeDestroy: [function () {}],
  name: 'HelloWorld',
  data () {
    return {
      msg: 'hello, world'
    }
  },
  ...
}

我们可以发现vue-loader默认添加的有beforeCreatebeforeDestroy两个配置,如果我们组件自身也提供了这两个配置的话,这种情况必须进行配置合并。

  • extend:在上一节我们介绍createComponent的时候,我们知道子组件会继承大Vue上的一些属性或方法,假设我们全局注册了一个组件。
import HelloWorld from '@/components/HelloWorld.vue'
Vue.component('HelloWorld', HelloWorld)

当我们在其它组件中也注册了一些组件,这样大Vue上的components就要和组件中的components进行合理的配置合并。

  • mixin:在前面的配置合并背景小节中,我们使用Vue.mixin全局混入了两个生命周期配置,这属于mixin配置合并的范围,我们来举例另外一种组件内的mixin混入场景:
// mixin定义
const sayMixin = {
  created () {
    console.log('hello mixin created')
  },
  mounted () {
    console.log('hello mixin mounted')
  }
}

// 组件引入mixin
export default {
  name: 'App',
  mixins: [sayMixin],
  created () {
    console.log('app component created')
  },
  mounted () {
    console.log('app component mounted')
  }
}

当在App.vue组件中提供mixins选择的时候,因为在我们定义的sayMixin也提供了createdmounted两个生命周期配置,因此这种情况下也要进行配置合并。又因为mixins接受一个数组选项,假如我们传递了多个已经定义的mixin,而这些mixin又可能会存在提供了相同配置的情况,因此同样需要进行配置合并。

注意Vue.mixin全局API方法在内部调用了mergeOptions来进行混入,它的定义位置我们在之前的initGlobalAPI小节中提到过,其实现代码如下:

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
  • this._init:严格意义上来说,这里其实并不算是一个配置合并的场景,而应该是一种配置合并的手段。对于第一种vue-loader和第二种extend的场景,它们在必要的场景下也会在this._init进行配置合并,例如在子组件实例化的时候,它在构造函数中就调用了this._init:
const Sub = function VueComponent (options) {
  this._init(options)
}

Vue.prototype._init = function () {
  // ...省略其它
  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
    )
  }
  // ...省略其它
}

合并策略

我们先来看看合并策略的代码,它是定义在src/core/util/options.js文件中,其代码如下:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    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.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

我们先忽略mergeOptions方法中其它的代码,来看最核心的mergeField,在这个方法里面,它会根据不同的key,调用策略对象strats中的策略方法,然后把合并完的配置再赋值到options上,strats策略对象每个key的具体定义我们会在之后对应的章节中介绍。

默认合并策略

mergeField方法中,我们看到当传入的key没有对应的策略方法时,会使用defaultStrat默认合并策略,它的定义代码如下:

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

defaultStrat默认合并策略的代码非常简单,即:简单的覆盖已有值,例如:

const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
}
const parent = {
  age: 23,
  name: 'parent',
  sex: 1
}
const child = {
  age: undefined,
  name: 'child',
  address: '广州'
}
function mergeOptions (parent, child) {
  let options = {}
  for (const key in parent) {
    mergeField(key)
  }
  for (const key in child) {
    if (!parent.hasOwnProperty(key)) {
      mergeField(key)
    }
  }

  function mergeField (key) {
    options[key] = defaultStrat(parent[key], child[key])
  }
  return options
}
const $options = mergeOptions(parent, child)
console.log($options) // { age: 23, name: 'child', sex: 1, address: '广州' }

代码分析:在以上案例中,agename都存在于parentchild对象中,因为child.age值为undefined,所以最后取parent.age值,这种情况也适用于sex属性的合并。因为child.name值不为undefined,所以最后取child.name的值,这种情况也适用于address属性的合并。

注意:如果你想针对某一个选项修改它的默认合并策略,可以使用Vue.config.optionMergeStrategies去配置,例如:

// 自定义el选择的合并策略,只取第二个参数的。
import Vue from 'vue'
Vue.config.optionMergeStrategies.el = (toVal, fromVal) {
  return fromVal
}

el和propsData合并

对于elpropsData属性的合并,在Vue中使用了默认合并策略,其定义代码如下:

const strats = config.optionMergeStrategies
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    // ...省略其它
    return defaultStrat(parent, child)
  }
}

对于elpropsData这两个选项来说,使用默认合并策略的原因很简单,因为elpropsData只允许有一份。

生命周期hooks合并

对于生命周期钩子函数而言,它们都是通过mergeHook方法来合并的,strats策略对象上关于hooks属性定义代码如下:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

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

我们接下来看一下mergeHook是如何实现的,其代码如下:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

我们可以看到在mergeHook方法中,它用到了三层三目运算来判断,首先判断了是否有childVal,如果没有则直接返回parentVal;如果有,再判断parentVal有没有,如果有则一定是数组形式,这个时候直接把childVal添加到parentVal数组的末尾;如果没有,则需要判断一下childVal是不是数组,如果不是数组则转成数组,如果已经是数组了,则直接返回。

在最后还判断了res,然后满足条件则调用dedupeHooks,这个方法的作用很简单,就是剔除掉数组中的重复项。最后,我们根据以上逻辑撰写几个案例来说明。

// 情况一
const parentVal = [function created1 () {}]
const childVal = undefined
const result = [function created1 () {}]

// 情况二
const parentVal = [function created1 () {}]
const childVal = [function created2 () {}]
const result = [function created1 () {}, function created2 () {}]

// 情况三
const parentVal = undefined
const childVal = [function created2 () {}]
const result = [function created2 () {}]

我们再来看一个比较特殊的场景:

// mixin.js
export const sayMixin = {
  created () {
    console.log('say mixin created')
  }
}
export const helloMixin = {
  created () {
    console.log('hello mixin created')
  }
}


// App.vue
import { sayMixin, helloMixin } from './mixin.js'
export default {
  name: 'App',
  mixins: [sayMixin, helloMixin],
  created () {
    console.log('component created')
  }
}

// 执行顺序
// say mixin created
// hello mixin created
// component created

代码分析:我们可以看到mixins里面的created生命周期函数会优先于组件自身提供的created生命周期函数,这是因为在遍历parentchild的属性之前,会优先处理extendsmixins选项。以mixins为例,它会首先遍历我们提供的mixins数组,然后依次把这些配置按照规则合并到parent上,最后在遍历child的属性时,才会把其自身的配置合并对应的位置,在我们提供的例子当中,自身提供的created会使用数组concat方法添加到数组的末尾。当组件触发created生命周期的时候,会按照数组顺序依次调用。

if (!child._base) {
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}

data和provide合并

对于dataprovide而言,它们最后都使用mergeDataOrFn来合并,只不过对于data选项比较特殊,它需要单独包裹一层,它们在strats策略对象上的属性定义如下:

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && 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)
}
strats.provide = mergeDataOrFn

在合并data的包裹函数中,对childVal进行了检验,如果不是函数类型,提示错误信息并直接返回。如果时,再调用mergeDataOrFn方法来合并。接下来,我们来看一下mergeDataOrFn方法的具体实现逻辑:

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    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
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

mergeDataOrFn方法中,我们可以发现它根据vm进行了区分,但这两块的合并思路是一致的:如果parentValchildVal是函数类型,则分别调用这个函数,然后合并它们返回的对象,这种情况主要针对data合并。对于provide而言,它不需要是function类型,因此直接使用mergeData来合并即可。我们再回过头来看,为什么要区分vm,这是因为要处理兼容provide的情况,当传递provide的时候,因为这个属性是在父级定义的,因此this属于父级而不是当前组件vm

最后来看一下mergeData方法的实现代码:

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    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
}

mergeData和前面提到extend方法所做的事情几乎是一样的,只不过由于data中所有的属性(包括嵌套对象的属性),我们需要使用set处理成响应式的。set方法就是Vue.setthis.$set方法的本体,它定义在src/core/observer/index.js文件中,我们之前在响应式章节提到过。

components、directives和filters合并

对于componentsdirectives以及filters的合并是同一个mergeAssets方法,strats策略对象上关于这几种属性定义代码如下:

const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

接下来,我们看一下mergeAssets具体定义:

function mergeAssets (
  parentVal: ?Object,1
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

mergeAssets方法的代码不是很多,逻辑也很清晰,首先以parentVal创建一个res原型,如果childVal没有,则直接返回这个res原型;如果有,则使用extendchildVal上的所有属性扩展到res原型上。有一点需要注意,extend不是我们之前提到的Vue.extend或者this.$extend,它是定义在src/shared/utils.js文件中的一个方法,其代码如下:

export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

我们撰写一个简单的例子来说明一下extend方法的用法:

const obj1 = {
  name: 'AAA',
  age: 23
}
const obj2 = {
  sex: '男',
  address: '广州'
}
const extendObj = extend(obj1, obj2)
console.log(extendObj) // { name: 'AAA', age: 23, sex: '男', address: '广州' }

在介绍完extend方法后,我们回到mergeAssets方法,我们同样举例说明:

// main.js
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'
Vue.component('HelloWorld', HelloWorld)

// App.vue
import Test from '@/components/test.vue'
export default {
  name: 'App',
  components: {
    Test
  }
}

main.js入口文件中,我们全局定义了一个HelloWorld 全局组件,然后在App.vue中又定义了一个Test局部组件,当代码运行到mergeAssets的时候,部分参数如下:

const parentVal = {
  HelloWorld: function VueComponent () {...},
  KeepAlive: {...},
  Transition: {...},
  TransitionGroup: {...}
}
const childVal = {
  Test: function VueComponent () {...}
}

因为parentValchildVal都有值,因此会调用extend方法,调用前和调用后的res如下所示:

// 调用前
const res = {
  __proto__: {
    HelloWorld: function VueComponent () {...},
    KeepAlive: {...},
    Transition: {...},
    TransitionGroup: {...}
  }
}

// extend调用后
const res = {
  Test: function VueComponent () {...},
  __proto__: {
    HelloWorld: function VueComponent () {...},
    KeepAlive: {...},
    Transition: {...},
    TransitionGroup: {...}
  }
}

假如我们在App.vue组件中都使用了这两个组件,如下:

<template>
  <div>
    <test />
    <hello-world />
  </div>
</template>

App.vue组件渲染的过程中,当编译到<test />时,会在其components选项中查找组件,马上在自身属性上找到了test.vue。然后当编译到<hello-world />的时候,在自身对象上找不到这个属性,根据原型链的规则会在原型上去找,然后在__proto__上找到了HelloWorld.vue组件,两个组件得以顺利的被解析和渲染。

对于另外两个选项directivesfilters,它们跟components是一样的处理逻辑。

watch合并

对于watch选项而言,它使用的合并方法是单独定义的,其在strats策略对象上的属性定义如下:

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

我们可以看到watch配置的合并与hooks合并的思路几乎差不多,只是多了一些微小的差异,当childVal没有时,直接返回按照parentVal创建的原型,类似的当parentVal没有时,直接返回childVal,注意这里因为是自身的配置,因此不需要像parentVal那样创建并一个原型。当parentValchildVal都存在时,首先把parentVal上的属性全部扩展到ret对象上,然后遍历childVal的属性键。在遍历的过程中如果parent值不为数组形式,则手动处理成数组形式,然后把child使用数组concat方法添加到数组的末尾。以上代码分析,可以使用下面的示例来说明:

// 情况一
const parentVal = {
  msg: function () {
    console.log('parent watch msg')
  }
}
const childVal = undefined
const ret = {
  __proto__: {
    msg: function () {
      console.log('parent watch msg')
    }
  }
}

// 情况二
const parentVal = undefined
const childVal = {
  msg: function () {
    console.log('child watch msg')
  }
}
const ret = {
  msg: function () {
    console.log('child watch msg')
  }
}

// 情况三
const parentVal = {
  msg: function () {
    console.log('parent watch msg')
  }
}
const childVal = {
  msg: function () {
    console.log('child watch msg')
  }
}
const ret = {
  msg: [
    function () {
      console.log('parent watch msg')
    },
    function () {
      console.log('child watch msg')
    }
  ]
}

hooks一样,如果在mixins里面也提供了与自身组件一样的watch,那么会优先执行mixins里面的watch,然后在执行自身组件中的watch

props、methods、inject和computed合并

propsmethodsinjectcomputed和之前我们提到的几种配置有点不一样,这几种配置有一个共同点:不允许存在相同的属性,例如我们在methods上提供的属性,不管来自于哪里,我们只需要把所有属性合并在一起即可。

接下来我们来看一下这几个属性在strats策略对象上的具体定义:

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}

我们可以看到,在其实现方法中代码并不是很复杂,仅仅使用到extend方法合并对象属性即可。当parentVal没有时,直接返回childVal,这里也不需要创建并返回一个原型,原因在上面提到过。如果parentVal有,则先创建一个原型,再使用extendparentVal上的所有属性全部扩展到ret对象上。最后再判断childVal,如果有则再使用extendchildVal上的对象扩展到ret上,如果没有,则直接返回。以上代码分析,我们举例说明:

const parentVal = {
  age: 23,
  name: 'AAA'
}
const childVal = {
  address: '广州'
}
const ret = {
  age: 23,
  name: 'AAA',
  address: '广州'
}
最后更新时间:
贡献者: wangtunan