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:
<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
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
<code>/**
* Default strategy.
*/
const defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
}
</code>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]
<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
eland
propsDatause 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
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.
<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
mergeDatarecursively 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.
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.