# 合并策略

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

# 背景

我们可以会很好奇,为什么要进行配置合并?这是因为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又可能会存在提供了相同配置的情况,因此同样需要进行配置合并。

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

# 合并策略

我们先来看看合并策略的代码,它是定义在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合并

# data和provide合并

# components、directives和filters合并

# watch合并

# props、methods、inject和computed合并

最后更新时间: 2/28/2023, 8:33:37 PM