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 / mixins options, the core of mergeOptions is the definition of merge strategies.
The final part of the mergeOptions source 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 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
/**
* Default strategy.
*/
const defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
}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]
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 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
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 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.
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 mergeData recursively 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
