什么是选项合并策略
在Vue中,使用mixins的时候发现:
- 混入的methods里的方法如果与组件里的方法同名时,会被组件的方法覆盖;
- 但生命周期函数如果重名,混入的生命周期函数与组件本身的都会被执行,执行顺序为先执行混入后执行自身。
这主要与Vue中的选项合并策略有关,不同的选项有不同的合并策略。
例如:
data、props、methods都是同属性直接覆盖合并;
created、mounted等生命周期函数都是直接合并,同名的函数存放在一个数组中,依次进行调用。
Vue提供一个api:
Vue.config.optionMergeStrategies
可以通过这个api实现自定义的选项合并策略。
在代码中打印:
console.log(Vue.config.optionMergeStrategies)
可以看出,不同的选项都有各自的合并策略
tips:以下父组件选项可理解为mixins混入的数据;子组件选项为当前自身组件选项。
默认合并策略 defaultStrat
解读源码:
传入两个参数parentVal、childVal分别对应父组件和子组件的选项;
采用的合并的策略为子组件选项不存在时返回父组件选项,否则返回子组件。
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal
}
el、propsData
以此可以看出:el和propsData的合并策略就是属于默认合并策略。
/**
* Options with restrictions
*/
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)
}
}
props、methods、computed、inject
源码解读:
若父组件选项不存在,直接返回子组件选项;
否则,创建一个空对象ret,父组件选项覆盖ret属性;若子组件选项存在,则覆盖父组件选项中同名的属性。
即:父子组件选项中存在同名属性,则子组件选项属性覆盖父组件选项属性。
/**
* Other object hashes.
*/
strats.props =
strats.methods =
strats.inject =
strats.computed =
function (parentVal: ?Object, childVal: ?Object, vm?: Component, key: string): ?Object {
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
}
/** 遍历合并数据,子组件选项同名属性覆盖父组件
* Mix properties into target object.
*/
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
data
源码解读:
如果传入参数‘vm’,则表示组件为根实例。
若子组件选项‘data’存在但是不为function类型,直接返回父组件选项;
否则进行合并策略。
strats.data = function (parentVal: any, childVal: any, vm?: Component): ?Function {
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)
}
源码解读:
若子组件data不存在,返回父组件data;若父组件data不存在,返回子组件data。
进行合并策略。
/**
* Data
*/
export function mergeDataOrFn (parentVal: any, childVal: any, vm?: Component): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// 若组件选项类型为function,需要使用call()指向当前this
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// parent为根实例
return function mergedInstanceDataFn () {
// instance merge
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
}
}
}
}
源码解读:
递归合并两个数据对象
/**
* Helper that recursively merges two data objects together.
*/
function mergeData (to: Object, from: ?Object): Object {
// 若不存在父组件数据,无需合并,直接返回子组件数据
if (!from) return to
let key, toVal, fromVal
// 获取父组件数据属性对应key值,返回一个key集合数组
const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from)
// 遍历父组件数据keys
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
// 获取子组件属性key的value值
toVal = to[key]
// 获取父组件属性key的value值
fromVal = from[key]
// 如果子组件数据对象中不存在该key,如果存在则不做改动
if (!hasOwn(to, key)) {
// 将父组件key对应的value值赋值给子组件数据属性key
set(to, key, fromVal)
// 如果父子组件当前key对应的value不相等,且为Object类型时,进行递归
} else if (toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal)
}
}
return to
}
// 判断浏览器是否支持某一方法 [native code] 如ES6新特性‘Symbol’、‘Reflect’等
/* istanbul ignore next */
export function isNative (Ctor: any): boolean {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
export const hasSymbol =
typeof Symbol !== 'undefined' && isNative(Symbol) &&
typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)
/**
* Get the raw type string of a value, e.g., [object Object].
*/
const _toString = Object.prototype.toString
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
watch
源码解读:
实现合并策略,不进行数据覆盖;合并在一个数组,先执行父组件数据,再执行子组件数据。
/**
* Watchers.
* Watchers hashes should not overwrite one
* another, so we merge them as arrays.
*/
strats.watch = function (parentVal: ?Object, childVal: ?Object, vm?: Component, key: string): ? Object {
// work around Firefox's Object.prototype.watch...
// 此处nativeWatch相当于undefined
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
// 如果子组件数据不存在,返回父组件数据对象 || null
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) {
// 获取父组件数据对应key的value值
let parent = ret[key]
// 获取子组件数据对应key的value值
const child = childVal[key]
// 如果父组件value值存在,但不为数组类型;将其设置为数组
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
// 如果父组件数据value存在,则拼接子组件数据value,合并成一个数组;
// 如果父组件数据value不存在,则判断子组件数据value是否为数组类型,若不是则将其设置为数组
ret[key] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]
}
return ret
}
// Firefox has a "watch" function on Object.prototype...
// nativeWatch = undefined
export const nativeWatch = ({}).watch
function assertObjectType (name: string, value: any, vm: ?Component) {
if (!isPlainObject(value)) {
warn(
`Invalid value for option "${name}": expected an Object, ` +
`but got ${toRawType(value)}.`,
vm
)
}
}
其他生命周期钩子
源码解读:
如果父子组件数据只有一个存在,则返回该数据,并以数组的形式;
如果父子组件数据同时存在,则合并两个数据,以数组的形式;父组件数据在前,子组件数据在后;
依次执行父子组件数据。
// 生命周期函数
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
];
/**
* Hooks and props are merged as arrays.
*/
// 如果子组件数据不存在,则返回父组件数据。
// 否则,如果父组件不存在则返回子组件数据;否则合并父子组件数据。
function mergeHook(parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function>): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});