Frontend Development 13 min read

Understanding Vue.js mergeOptions: How Merge Strategies Work

This article deeply analyzes Vue.js's mergeOptions function, explaining how merge strategies are defined and applied to options like data, props, hooks, assets, and watchers, with detailed code examples and step‑by‑step breakdowns to help developers understand Vue's option merging mechanics.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Understanding Vue.js mergeOptions: How Merge Strategies Work

Yuan Dong, a front‑end engineer at WeDoctor, shares an in‑depth analysis of Vue.js's option merging mechanism.

Definition of Merge Strategies

After standardizing props, inject, directives, and the

extends

/

mixins

options, the core of

mergeOptions

is the definition of merge strategies.

The final part of the

mergeOptions

source code is:

<code>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)
}
</code>

Both

parent

and

child

objects are iterated, and for each key the

mergeField()

function is called.

mergeField()

selects a strategy from

strats[key]

or falls back to

defaultStrat

and applies it.

defaultStrat

<code>/**
 * Default strategy.
 */
const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
}
</code>

The logic of

defaultStrat

is simple: if the child provides a value, use it; otherwise fall back to the parent value, giving priority to the child.

strats[key]

<code>const strats = config.optionMergeStrategies
export type Config = {
  // user
  optionMergeStrategies: { [key: string]: Function };
  ...
}
</code>

Developers can define custom merge functions for specific keys in

config.optionMergeStrategies

.

config.optionMergeStrategies.el, config.optionMergeStrategies.propsData

<code>if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(`option "${key}" can only be used during instance creation with the `new` keyword.`)
    }
    return defaultStrat(parent, child)
  }
}
</code>

Both

el

and

propsData

use the default strategy: the child value wins if present, otherwise the parent value is used.

config.optionMergeStrategies.data, config.optionMergeStrategies.provide

<code>strats.data = function (parentVal, childVal, vm) {
  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
</code>

The

data

and

provide

strategies delegate to

mergeDataOrFn

, handling both constructor‑level and instance‑level merges.

If no

vm

(i.e., the component is created via

Vue.extend()

), the function checks whether

childVal

is a function and merges accordingly.

If a

vm

instance exists (i.e., created with

new Vue()

), the instance data is merged with the constructor data.

<code>export function mergeDataOrFn (parentVal, childVal, vm) {
  if (!vm) {
    // 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 () {
      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
      }
    }
  }
}
</code>

The helper

mergeData

recursively merges two data objects, skipping the internal

__ob__

marker.

<code>function mergeData (to, from) {
  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
}
</code>

Hook Merging Strategy

<code>export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
function mergeHook (parentVal, childVal) {
  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
}
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})
</code>

Each lifecycle hook is merged as an array; duplicate functions are removed by

dedupeHooks

.

Assets (components, directives, filters) Merging Strategy

<code>function mergeAssets (parentVal, childVal, vm, key) {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
ASSET_TYPES.forEach(type => {
  strats[type + 's'] = mergeAssets
})
</code>

If the child defines these assets, they are merged with the parent; otherwise the parent assets are returned unchanged.

props / methods / inject / computed Merging Strategy

<code>strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal, childVal, vm, key) {
  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
}
</code>

The strategy creates a new object, copies the parent options, then merges the child options if present.

watch Merging Strategy

<code>strats.watch = function (parentVal, childVal, vm, key) {
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  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
}
</code>

Watchers are merged as arrays, ensuring that multiple handlers for the same key are combined.

Conclusion

The three‑part series thoroughly dissects Vue.js's option merging process, covering data, hooks, assets, props, methods, inject, computed, and watch. Understanding these strategies helps developers customize component behavior and debug complex inheritance scenarios.

frontendJavaScriptVue.jsmergeOptionsoption merging
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.