Skip to content

vue3中options选项的合并策略

vue3中通过resolveMergedOptions函数进行合并options选项。

resolveMergedOptions

ts
export function resolveMergedOptions(
  instance: ComponentInternalInstance
): MergedComponentOptions {
  // base就是组件,其中包含了组件的options
  const base = instance.type as ComponentOptions
  // 获取组件中的局部mixins与extends
  const { mixins, extends: extendsOptions } = base
  // 获取全局mixins、options的缓存对象、合并策略
  const {
    mixins: globalMixins,
    optionsCache: cache,
    config: { optionMergeStrategies }
  } = instance.appContext
  // 根据组件从缓存对象中获取已经合并好的options
  const cached = cache.get(base)

  let resolved: MergedComponentOptions
  
  // 如果存在已经合并好的options,则将cached赋值给resolved
  if (cached) {
    resolved = cached
  } else if (!globalMixins.length && !mixins && !extendsOptions) { // 未获得缓存options,并且不存在全局mixins、局部mixins、extends,base即为resolved
    if (
      __COMPAT__ &&
      isCompatEnabled(DeprecationTypes.PRIVATE_APIS, instance)
    ) {
      resolved = extend({}, base) as MergedComponentOptions
      resolved.parent = instance.parent && instance.parent.proxy
      resolved.propsData = instance.vnode.props
    } else {
      resolved = base as MergedComponentOptions
    }
  } else { // 其他情况,说明存在全局mixins或局部mixins或extends
    resolved = {}
    // 遍历globalMixins,将遍历globalMixins中的mixins合并到resolved中
    if (globalMixins.length) {
      globalMixins.forEach(m =>
        mergeOptions(resolved, m, optionMergeStrategies, true)
      )
    }
    // 将base合并到resolved中
    mergeOptions(resolved, base, optionMergeStrategies)
  }
  // 将合并后的options对象缓存起来
  cache.set(base, resolved)
  return resolved
}

mergeOptions

ts
export function mergeOptions(
  to: any, // 合并到的目标对象
  from: any, // 被合并的对象
  strats: Record<string, OptionMergeFunction>, // 合并策略
  asMixin = false // 是否正在合并mixins或extends中的options
) {
  // 如果被合并的选项是个函数,则取函数的options属性
  if (__COMPAT__ && isFunction(from)) {
    from = from.options
  }

  // 如果from中还存在mixins或extends,递归调用mergeOptions
  const { mixins, extends: extendsOptions } = from
  if (extendsOptions) {
    mergeOptions(to, extendsOptions, strats, true)
  }
  if (mixins) {
    mixins.forEach((m: ComponentOptionsMixin) =>
      mergeOptions(to, m, strats, true)
    )
  }

  for (const key in from) {
    // mixins、extends中的expose会被忽略
    if (asMixin && key === 'expose') {
      __DEV__ &&
        warn(
          `"expose" option is ignored when declared in mixins or extends. ` +
            `It should only be declared in the base component itself.`
        )
    } else {
      // internalOptionMergeStrats中定义了组件options(如data、props、emits、created、watch等)的合并策略
      const strat = internalOptionMergeStrats[key] || (strats && strats[key])
      // 如果存在合并策略,调用合并策略生成合并结果,否则直接使用from中的值进行覆盖
      to[key] = strat ? strat(to[key], from[key]) : from[key]
    }
  }
  return to
}

如果组件中不存在全局mixins、局部mixinsextends,则不需要合并options操作。相反,首先合并全局mixins到一个空对象中,然后依次将extends、局部mixins中的options合并到这个对象中。处理好的options对象会被缓存到instance.appContext.optionCache中,以便后续使用。

对于组件内置的options选项,其合并策略都保存在internalOptionMergeStrats中。

ts
export const internalOptionMergeStrats: Record<string, Function> = {
  data: mergeDataFn,
  props: mergeObjectOptions, // TODO
  emits: mergeObjectOptions, // TODO
  // objects
  methods: mergeObjectOptions,
  computed: mergeObjectOptions,
  // lifecycle
  beforeCreate: mergeAsArray,
  created: mergeAsArray,
  beforeMount: mergeAsArray,
  mounted: mergeAsArray,
  beforeUpdate: mergeAsArray,
  updated: mergeAsArray,
  beforeDestroy: mergeAsArray,
  beforeUnmount: mergeAsArray,
  destroyed: mergeAsArray,
  unmounted: mergeAsArray,
  activated: mergeAsArray,
  deactivated: mergeAsArray,
  errorCaptured: mergeAsArray,
  serverPrefetch: mergeAsArray,
  // assets
  components: mergeObjectOptions,
  directives: mergeObjectOptions,
  // watch
  watch: mergeWatchOptions,
  // provide / inject
  provide: mergeDataFn,
  inject: mergeInject
}

if (__COMPAT__) {
  internalOptionMergeStrats.filters = mergeObjectOptions
}

options合并策略

dataprovide合并策略

dataprovide合并策略通过mergeDataFn函数实现。

ts
function mergeDataFn(to: any, from: any) {
  if (!from) {
    return to
  }
  if (!to) {
    return from
  }
  return function mergedDataFn(this: ComponentPublicInstance) {
    return (
      __COMPAT__ && isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null)
        ? deepMergeData
        : extend
    )(
      isFunction(to) ? to.call(this, this) : to,
      isFunction(from) ? from.call(this, this) : from
    )
  }
}

export function deepMergeData(to: any, from: any) {
  for (const key in from) {
    const toVal = to[key]
    const fromVal = from[key]
    if (key in to && isPlainObject(toVal) && isPlainObject(fromVal)) {
      __DEV__ && warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, null, key)
      deepMergeData(toVal, fromVal)
    } else {
      to[key] = fromVal
    }
  }
  return to
}

export const extend = Object.assign

dataprovide的合并策略会受到兼容模式及OPTIONS_DATA_MERGE的影响。

在兼容模式下,如果OPTIONS_DATA_MERGEtrue,会将data进行深度合并,否则进行浅层合并(只合并根级属性)。非兼容模式下,即vue3中,是浅层合并。

示例:

  1. 使用兼容vue2的版本。user会被进行深拷贝。如果使用configureCompatOPTIONS_DATA_MERGE修改为falseuser会被浅拷贝。
html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/@vue/compat@3.2.37/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app"></div>

<script type="module">
  import { createApp, configureCompat } from 'vue'

  configureCompat({
    OPTIONS_DATA_MERGE: false
  })
  
  createApp({
    mixins: [
      {
        data() {
          return {
            user: {
              name: 'Tom',
              id: 1
            }
          }
        }
      }
    ],
    data() {
      return {
        user: {
          id: 2
        }
      }
    },
    mounted() {
      // { user: { id: 2 } }
      console.log(this.$data.user)
    }
  }).mount('#app')
</script>
  1. 使用非兼容版本的vue3user被浅拷贝
html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app"></div>

<script type="module">
  import { createApp, configureCompat } from 'vue'

  configureCompat({
    OPTIONS_DATA_MERGE: false
  })
  createApp({
    mixins: [
      {
        data() {
          return {
            user: {
              name: 'Tom',
              id: 1
            }
          }
        }
      }
    ],
    data() {
      return {
        user: {
          id: 2
        }
      }
    },
    mounted() {
      // { user: { id: 2 } }
      console.log(this.$data.user)
    }
  }).mount('#app')
</script>

propsemitsmethodscomputedcomponentsdirectivesfilters合并策略

propsemitsmethodscomputedcomponentsdirectivesfilters的合并策略均是通过mergeObjectOptions函数进行实现。

ts
function mergeObjectOptions(to: Object | undefined, from: Object | undefined) {
  return to ? extend(extend(Object.create(null), to), from) : from
}

propsemitsmethodscomputedcomponentsdirectivesfilters这些options有个共同特点:它们都是一个对象(emits可以是数组)。它们的合并策略很简单,就是使用Object.assign进行浅拷贝。

示例

html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app">
  <button  @click="handleClick">click me</button>
</div>

<script type="module">
  import { createApp } from 'vue'

  createApp({
    extends: {
      methods: {
        handleClick() {
          console.log('from extends')
        }
      }
    },
    mixins: [
      {
        methods: {
          handleClick() {
            console.log('from mixins')
          }
        }
      }
    ],
    methods: {
      handleClick() {
        console.log('from component self')
      }
    }
  }).mount('#app')
</script>

当点击按钮,控制台打印from component self。如果将组件自身的handleClick注释掉,再点击按钮,控制台打印from mixins。这是因为options合并的顺序是全局mixinsextends、局部mixins、组件自身options,那么options的优先级就是这个顺序的倒序。

生命周期钩子options合并策略

生命周期钩子相关options的合并策略均是通过mergeAsArray函数完成的。

ts
function mergeAsArray<T = Function>(to: T[] | T | undefined, from: T | T[]) {
  return to ? [...new Set([].concat(to as any, from as any))] : from
}

生命周期钩子相关options,会被合并至一个去重的数组中,数组中的顺序依次为:全局mixinsextends、局部mixins、组件自身options

示例

html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app"></div>

<script type="module">
  import { createApp } from 'vue'
  
  const app = createApp({
    mixins: [
      {
        beforeCreate() {
          console.log('from component mixins')
        }
      }
    ],
    extends: {
      beforeCreate() {
        console.log('from extends')
      }
    },
    beforeCreate() {
      console.log('from component self')
    }
  })
  
  app.mixin({
    beforeCreate() {
      console.log('from global mixins')
    }
  })
  
  app.mount('#app')
</script>

上述代码依次打印:from global mixinsfrom extendsfrom component mixinsfrom component self

watch合并策略

watch的合并策略通过mergeWatchOptions函数实现。

ts
function mergeWatchOptions(
  to: ComponentWatchOptions | undefined,
  from: ComponentWatchOptions | undefined
) {
  if (!to) return from
  if (!from) return to
  const merged = extend(Object.create(null), to)
  for (const key in from) {
    merged[key] = mergeAsArray(to[key], from[key])
  }
  return merged
}

watch的合并策略与生命周期钩子的合并策略相同,相同keywatcher会被合并到一个去重的数组中。

示例

html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app">
  <button @click="handleClick">click me</button>
</div>

<script type="module">
  import { createApp, ref } from 'vue'

  const app = createApp({
    mixins: [
      {
        watch: {
          count() {
            console.log('from component mixins')
          }
        }
      }
    ],
    extends: {
      watch: {
        count() {
          console.log('from extends')
        }
      }
    },
    watch: {
      count() {
        console.log('from component self')
      }
    },
    setup() {
      const count = ref(0)
      function handleClick() {
        count.value++
      }
      return {
        count,
        handleClick
      }
    }
  })

  app.mixin({
    watch: {
      count() {
        console.log('from global mixins')
      }
    }
  })
  
  app.mount('#app')
</script>

inject合并策略

inject的合并通过mergeInject函数实现。

ts
function mergeInject(
  to: ComponentInjectOptions | undefined,
  from: ComponentInjectOptions
) {
  return mergeObjectOptions(normalizeInject(to), normalizeInject(from))
}

function normalizeInject(
  raw: ComponentInjectOptions | undefined
): ObjectInjectOptions | undefined {
  if (isArray(raw)) {
    const res: ObjectInjectOptions = {}
    for (let i = 0; i < raw.length; i++) {
      res[raw[i]] = raw[i]
    }
    return res
  }
  return raw
}

inject的合并与propscomputed等类似,也是通过mergeObjectOptions方法进行浅拷贝,不同的是,由于inject可以是数组,所以需要调用normalizeInjectinject标准化为对象。

总结

options的合并顺序:全局mixins -> extends -> 局部mixins -> 组件自身options

options合并策略说明
dataprovide可进行深度合并,也可进行浅层合并。取决于是否在兼容模式中,及OPTIONS_DATA_MERGE的值。在兼容模式中,如果MODE2vue2模式),默认会进行深度合并;如果MODE3vue3模式),默认进行浅层合并。两种模式下都可以通过更改OPTIONS_DATA_MERGEfalse为浅层合并,true为深层合并)的值改变合并策略。
propsemitsmethodscomputedcomponentsdirectivesfilters浅层合并
beforeCreate等生命周期钩子合并至一个去重的数组中执行顺序与options合并顺序相同
watch相同keywatcher合并至一个去重的数组中执行顺序与options合并顺序相同
inject浅层拷贝由于inject可能是数组,所以合并前需要标准化为对象
vue3中options选项的合并策略 has loaded