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.
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/
mixinsoptions, the core of
mergeOptionsis the definition of merge strategies.
The final part of the
mergeOptionssource code is:
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)
}Both
parentand
childobjects are iterated, and for each key the
mergeField()function is called.
mergeField()selects a strategy from
strats[key]or falls back to
defaultStratand applies it.
defaultStrat
/**
* Default strategy.
*/
const defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
}The logic of
defaultStratis simple: if the child provides a value, use it; otherwise fall back to the parent value, giving priority to the child.
strats[key]
const strats = config.optionMergeStrategies
export type Config = {
// user
optionMergeStrategies: { [key: string]: Function };
...
}Developers can define custom merge functions for specific keys in
config.optionMergeStrategies.
config.optionMergeStrategies.el, config.optionMergeStrategies.propsData
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)
}
}Both
eland
propsDatause the default strategy: the child value wins if present, otherwise the parent value is used.
config.optionMergeStrategies.data, config.optionMergeStrategies.provide
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 = mergeDataOrFnThe
dataand
providestrategies 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
childValis a function and merges accordingly.
If a
vminstance exists (i.e., created with
new Vue()), the instance data is merged with the constructor data.
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
}
}
}
}The helper
mergeDatarecursively merges two data objects, skipping the internal
__ob__marker.
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
}Hook Merging Strategy
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
})Each lifecycle hook is merged as an array; duplicate functions are removed by
dedupeHooks.
Assets (components, directives, filters) Merging Strategy
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
})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
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
}The strategy creates a new object, copies the parent options, then merges the child options if present.
watch Merging Strategy
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
}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.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.