Vue 合并策略 optionMergeStrategies 看这里就够了
共 15293字,需浏览 31分钟
·
2021-07-11 13:32
鄢栋,微医前端技术部前端工程师。有志成为一名全栈开发工程师甚至架构师,路漫漫,吾求索。 生活中通过健身释放压力,思考问题。
推荐阅读
文章篇幅较长, 建议花整块时间阅读分析。 另外由于篇幅过长, 本文分三篇文章产出, 便于大家理解与阅读。
合并策略的定义
接着上一篇标准化 props, inject, directives, 以及传入选项的 extends, mixins 属性的合并, 本篇来讲解 mergeOptions 函数的核心部分: 合并策略的定义。
我们看 mergeOptions 函数的最后一部分源码:
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)
}
可以看到, 分别遍历 parent 和 child 对象, 对每一个 key 值都调用了mergeField()函数
mergeField()函数
是最终的合并策略函数。里面调用了strat()
函数, 而 strat 函数来自strats[key]
和defaultStrat
先看一下defaultStrat
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
所以defaultStrat
的逻辑是,如果 child 上该属性值存在时,就取 child 上的该属性值,如果不存在,则取 parent 上的该属性值。优先取 child 上的选项配置。
再看strats[key]
strats 是一个函数
const strats = config.optionMergeStrategies
export type Config = {
// user
optionMergeStrategies: { [key: string]: Function };
...
};
可以在 starts( config.optionMergeStrategies)上定义不同函数类型的 key,然后定义这些 key 对应的合并策略。
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)
}
}
从上述代码中可以看到, el 以及 propsData 的合并策略是默认的合并策略, 如果子组件的选项存在, 就取子组件的,否则取父组件的。
config.optionMergeStrategies.data、config.optionMergeStrategies.provide
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)
}
strats.provide = mergeDataOrFn
可以看到, data 以及 provide 合并策略调用了mergeDataOrFn
函数; 以下分析 data 选项的合并策略, provide 同理。
如果不是当前实例,即通过 Vue.extend()创建的实例 如果 childVal 不是函数, 则返回 parentVal 作为当前 data 合并后的结果 否则调用 mergeDataOrFn(parentVal, childVal)
, 这个时候未传入 vm 实例如果是当前实例,即通过 new Vue()创建的实例 调用 mergeDataOrFn(parentVal, childVal, vm)
, 这个时候传入了 vm 实例
找到 mergeDataFn 函数块
/**
* 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
}
// 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(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
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
}
}
}
}
小结
根据上面的分析很快我们就能找到:
如果没有传入 vm 当前实例, 也就是通过 Vue.extend()/Vue.component 创建的实例的时候 若没有 childVal, 有 parentVal, 则返回 parentVal 若没有 parentVal, 有 childVal, 则返回 childVal 若 childVal,parentVal 两者都有 data 选项, 则调用 mergeData()
函数合并 data 选项如果传入了 vm 实例, 也就是通过 new Vue()这种形式创建的实例 如果新建实例时传入了 data 选项,则调用 mergeData
函数合并实例和构造函数上的 data 选项如果新建实例时没有传入 data 选项, 则返回构造函数上的 data 选项
不管是哪种方式创建的实例, 最终都调用了mergeData
函数进行合并 data 选项。我们看一下mergeData
函数
/**
* Helper that recursively merges two data objects together.
*/
function mergeData (to: Object, from: ?Object): Object {
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
}
如果构造器(from)上没有 data 选项, 返回实例(to)上的 data 选项 如果构造器上也有 data 选项 先返回构造器 from 的 data 上所有的 key 值(keys) 遍历构造器 data 的 keys 如果实例 to 的 data 选项上没有构造器 data 选项上的 key 值, 则调用 set 方法将该(key, fromVal)键值对挂到实例对象 to 的 data 选项里 否则, 如果 to 的 data 选项与构造器上的 data 选项有相同的 key 值, 并且该 key 对应的值是对象, 则递归调用 mergeData 函数 最后返回实例 to 上的 data 选项
钩子函数合并的策略
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
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
})
我们看到,每一个钩子合并都是调用了 mergeHook 函数,mergeHook 的逻辑
获取钩子数组 res 如果 child options 上不存在这个钩子,parent 存在, 就返回 parent 上的钩子 如果 child, parent 上都存在相同的钩子, 则返回 concat 之后的属性 child options 上存在, parent 上不存在, 则判断 child 上的该属性是数组, 则直接返回 child 上该属性 如果 res 不存在, 返回 res 否则返回 dedupeHooks(res), 去重后的 hooks
Assets(components, directives, filters)的合并策略
/**
* 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,
childVal: ?Object,
vm?: Component,
key: string
): Object {
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(function (type) {
strats[type + 's'] = mergeAssets
})
处理逻辑:如果 child 上存在这些选项,则与构造器 parent 上的选项合并, 否则直接返回构造器上的这些 types 选项
props/methods/inject/computed 的合并策略
/**
* 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
}
这个合并方法的逻辑也很简单:
如果构造器上没有该选项, 直接返回实例上的选项 如果构造器上有, 实例上也有, 返回合并后的结果
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...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
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
}
逻辑处理同样也是三种:
如果实例上没有 watch 选项, 返回构造器 parent 上的 watch 选项或者 null 否则如果实例上有, 构造器上没有, 就返回实例上的 watch 选项 否则两者都有的时候, 遍历实例上 watch 对象 如果 parent 上该 key 对应的值不是数组, 则返回[parent] 合并 parent 和 child 的 watch 选项 如果 parent 上存在与 child 中 watch 的 key 如果 parent 上不存在与 child 中 watch 的 key 返回数组形式的 child
写在最后
终于熬完了合并选项的分析(三篇文章), 每天抽出睡觉的时间来啃这块, 非常不容易,促进自己成长, 希望也能给小可爱们带去一些启发, 如果有地方觉得说的不清楚的欢迎在下方评论区提问,讨论~
看到这里的小可爱们, 可顺手点个赞鼓励我继续创作, 创作不易,一起学习, 早日实现财富自由~