本节我们主要总结 Vue
中的合并策略,涉及到 mixins
和 Vue.extend
的合并机制。
mergeOptions
方法(Vue
选项合并机制的核心):
1.mergeOptions
在 Vue
中,主要是利用 mergeOptions
这个入口函数来合并 options
选项。
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
function mergeOptions(
parent: Record<string, any>,
child: Record<string, any>,
vm?: Component | null
): ComponentOptions {
if (__DEV__) {
checkComponents(child)
}
if (isFunction(child)) {
// @ts-expect-error
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options: ComponentOptions = {} as any
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key: any) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
- 其中,
normalizeProps
、normalizeInject
以及normalizeDirectives
是格式化函数。
normalizeProps
会将数组或者对象类型的props
统一为对象格式;normalizeInject
会将数组或者对象类型的inject
统一为对象格式;normalizeDirectives
会将函数类型的directive
统一为对象格式{ bind: fn, update: fn }
(其实就是directive
的简写形式)。
- 然后对
extends
和mixins
进行合并。这里的代码其实是修正后的PR#8870。
child._base
的判断,保证了不进行重复合并。
最后就是对于
options
中字段的mergeField
合并,譬如data
、props
、methods
等等。这些字段的合并策略统一定义在了strats
也就是Vue.config.optionMergeStrategies
对象下。如果
strats[key]
策略不存在,则采用默认策略defaultStrat
。
2.合并函数
在开始说明 options
相关字段的合并策略之前,先介绍一下几个核心的合并函数。
2-1.defaultStrat
严格意义上来说,defaultStrat
并没有合并操作,但为了更好的对比,我将它也整合到这里。
逻辑很简单,就是存在 child
值就取 child
值,否则取 parent
值。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal
}
2-2.extend
extend
函数浅拷贝:
function extend(
to: Record<PropertyKey, any>,
_from?: Record<PropertyKey, any>
): Record<PropertyKey, any> {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
2-3.mergeData
mergeData
函数深拷贝,并且会将属性进行 set
响应式处理:
function mergeData(
to: Record<string | symbol, any>,
from: Record<string | symbol, any> | null,
recursive = true
): Record<PropertyKey, any> {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? (Reflect.ownKeys(from) as string[])
: 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 (!recursive || !hasOwn(to, key)) {
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
3.合并字段
3-1.el & propsData
el
和 propsData
选项合并采用 defaultStrat
策略,如果是开发环境下,会有额外的 key
判断提示:
if (__DEV__) {
strats.el = strats.propsData = function (
parent: any,
child: any,
vm: any,
key: any
) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
3-2.data
data
函数合并,会先判断下 data
是否是函数,然后指定函数 this
指向后将执行结果合并。
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): Function | null {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
__DEV__ &&
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)
}
/**
* Data
*/
function mergeDataOrFn(
parentVal: any,
childVal: any,
vm?: Component
): Function | null {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn() {
return mergeData(
isFunction(childVal) ? childVal.call(this, this) : childVal,
isFunction(parentVal) ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn() {
// instance merge
const instanceData = isFunction(childVal)
? childVal.call(vm, vm)
: childVal
const defaultData = isFunction(parentVal)
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
3-3.lifecycle hooks
lifecycle
钩子函数合并,会作为数组形式连接起来:
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeLifecycleHook
})
/**
* Hooks are merged as arrays.
*/
function mergeLifecycleHook(
parentVal: Array<Function> | null,
childVal: Function | Array<Function> | null
): Array<Function> | null {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: isArray(childVal)
? childVal
: [childVal]
: parentVal
return res ? dedupeHooks(res) : res
}
3-4.components & directive & filter
Assets
资源利用**原型链结合 extend
**进行合并:
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
/**
* Assets
*
* When a vm is present (instance creation), we need to do
* a three-way merge between constructor options, instance
* options and parent options.
*/
function mergeAssets(
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object {
// 这里利用原型链 组件寻找路径更加清晰 Child.components => Parent.components => Vue.options.components
const res = Object.create(parentVal || null)
if (childVal) {
__DEV__ && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
3-5.watch
watch
选项合并利用数组形式:
/**
* Watchers.
*
* Watchers hashes should not overwrite one
* another, so we merge them as arrays.
*/
strats.watch = function (
parentVal: Record<string, any> | null,
childVal: Record<string, any> | null,
vm: Component | null,
key: string
): Object | null {
// work around Firefox's Object.prototype.watch...
//@ts-expect-error work around
if (parentVal === nativeWatch) parentVal = undefined
//@ts-expect-error work around
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (__DEV__) {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret: Record<string, any> = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !isArray(parent)) {
parent = [parent]
}
ret[key] = parent ? parent.concat(child) : isArray(child) ? child : [child]
}
return ret
}
3-6.props & methods & inject & computed
props
、methods
、inject
以及 computed
利用**原型链结合 extend
**合并:
strats.props =
strats.methods =
strats.inject =
strats.computed =
function (
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object | null {
if (childVal && __DEV__) {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
3-7.provide
provide
利用**原型链结合 mergeData
**合并:
strats.provide = function (parentVal: Object | null, childVal: Object | null) {
if (!parentVal) return childVal
return function () {
const ret = Object.create(null)
mergeData(ret, isFunction(parentVal) ? parentVal.call(this) : parentVal)
if (childVal) {
mergeData(
ret,
isFunction(childVal) ? childVal.call(this) : childVal,
false // non-recursive
)
}
return ret
}
}