随着对Vue源码的深入,越来越觉得它的结构还是清晰的。想想也是这样的,如果不清晰,它们的开发人员也很难维护的呀。
众所周知在创建Vue实例是需要传递一个对象的,我们称之为选项(options)。最终每个实例会有一个 o p t i o n s 来 保 存 自 己 的 实 例 。 但 是 这 个 options来保存自己的实例。但是这个 options来保存自己的实例。但是这个options并不是创建Vue实例是通过构造函数传来的那个,它是综合了好几个层次的options产生出来的。这个综合的过程就是合并。
首先说一下,它综合了哪几个层次?先贴源码
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
单从这段代码上我们能知道它综合了组件构造函数和创建实例时传来的options。
- 组件类的options
- 创建实例时传来的options
如果再深入的话,组件类的options其实并不是简单的定义组件类时传入的options,而是综合了定义组件类传入的options和该组件类的父组件(一般就是Vue类)的options。
像我们平时全局注册一个组件,我们用来注册组件的那个其实只是组件的options(只是一个Object),只有调用了Vue.component(这个方法可以在core/global/assets.js中查看)才会把这个组件options转换成一个组件类(可以查看core/global-api/extend.js),创建的这个组件类会保存在Vue.components中,当然了Vue.component这方法也会返回这个组件类。
接下来说说合并的策略吧。options中有很多字段,各个字段的合并策略是不一样的。想让我们搞清楚options这个对象到底能接收哪些一级字段吧。我并没有找到源码里有明确定义了一个options对象的全部字段的地方,我只能从mergeOptions这个函数中总结了:
- data
- watch
- methods
- props
- inject
- computed
- provide
- 生命周期函数beforeCreate created beforeMount mounted beforeUpdate updated beforeDestroy destroyed activated deactivated
- 资源对象components filters directive
- 别的
提一句,这些合并策略放到了Vue.config.optionMergeStrategies中。
让我们一个一个的看下它们的合并策略吧
data和provide
data和provide要求是函数。两个不同options中的合并策略是:合并两个函数调用后返回的对象。下面是合并对象的策略。
/**
* Helper that recursively merges two data objects together.
*/
function mergeData (to: Object, from: ?Object): Object {
// 没有源对象,直接把目标对象返回
if (!from) return to
let key, toVal, fromVal
// 收集源对象的key
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
}
总结一下合并的data的过程
以源对象为基础,遍历源对象所有的key
- 如果目标对象没有这个key的话,就把这个key对应的值放到目标对象
- 如果目标对象有这个key,又有两种情况
- 源对象和目标对象中这个key对应的值相同,啥也不做
- 源对象和目标对象中这个key对应的值不相同且都是对象,则把它们俩合并
methods、props、inject、computed
先回忆一下methods、props、inject和computed在options中出现的形式,
- methods一定是对象形式的,而且没有嵌套
- computed一定是对象形式的,而且没有嵌套
- props和inject可以是字符串数组也可以是对象形式,字符串形式会被最终标准化对象形式的,因此最后到合并策略的时候也只是对象形式,而且没有嵌套
因此这四个的合并策略是一样的,是针对没嵌套的对象的合并,即综合两个对象中的字段,子组件options中的优先级高。
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
}
directives、filters和components
这三个在vue中都属于资源类,在shared/constants.js中定义了一个数组ASSET_TYPES,只有这三个。
说实话这个策略的结果跟上个策略的结果我觉得没有什么区别,最后都是子组件的options中的优先级高。
区别在于,上个策略的结果,每个字段都是属于返回的对象的,而这个策略的结果对象中的父组件的options是在返回对象的原型链上的。
生命周期函数的合并策略
生命周期函数有哪些呢?参考shared/constants.js中的LIFECYCLE_HOOKS
就是数组的合并,不存在覆盖的情况,各个options中的相同类型的hook函数综合到一个数组中,父组件的在前面子组件的在后面。
watch
我看了好几遍代码,watch的合并跟生命周期函数的合并效果是一样的呀
别的字段的策略
如果不在以上这些字段内的,都属于别的,就是优先取子组件的options内容,如果子options中没有就取父options中的内容。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
说一下mergeOptions的过程
传进来两个options让mergeOptions合并。进来第一件事就是把props、inject和directive进行标准化,都转换成对象形式的。
然后再处理子options上的extends和mixins。
最后对每一个key调用对应的合并策略进行合并!
PS: 如果有帮助,请点赞哦:)