1.5 合并策略
合并策略之所以是难点,其中一个是合并选项类型繁多,合并规则随着选项的不同也呈现差异。概括起来思路主要是以下两点:
Vue
针对每个规定的选项都有定义好的合并策略,例如data,component,mounted
等。如果合并的子父配置都具有相同的选项,则只需要按照规定好的策略进行选项合并即可。- 由于
Vue
传递的选项是开放式的,所有也存在传递的选项没有自定义选项的情况,这时候由于选项不存在默认的合并策略,所以处理的原则是有子类配置选项则默认使用子类配置选项,没有则选择父类配置选项。
我们通过这两个思想去分析源码的实现,先看看mergeOptions
除了规范检测后的逻辑。
function mergeOptions ( parent, child, vm ) {
···
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
// 如果有自定义选项策略,则使用自定义选项策略,否则选择使用默认策略。
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
两个for
循环规定了合并的顺序,以自定义选项策略优先,如果没有才会使用默认策略。而strats
下每个key
对应的便是每个特殊选项的合并策略
1.5.1 默认策略
我们可以用丰富的选项去定义实例的行为,大致可以分为以下几类:
- 用
data,props,computed
等选项定义实例数据 - 用
mounted, created, destoryed
等定义生命周期函数 - 用
components
注册组件 - 用
methods
选项定义实例方法
当然还有诸如watch,inject,directives,filter
等选项,总而言之,Vue
提供的配置项是丰富的。除此之外,我们也可以使用没有默认配置策略的选项,典型的例子是状态管理Vuex
和配套路由vue-router
的引入:
new Vue({
store, // vuex
router// vue-router
})
不管是插件也好,还是用户自定义的选项,他们的合并策略会遵循思路的第二点:**子配置存在则取子配置,不存在则取父配置,即用子去覆盖父。。**它的描述在defaultStrat
中。
// 用户自定义选项策略
var defaultStrat = function (parentVal, childVal) {
// 子不存在则用父,子存在则用子配置
return childVal === undefined
? parentVal
: childVal
};
接下来会进入某些具体的合并策略的分析,大致分为五类:
1. 常规选项合并
2. 自带资源选项合并
3. 生命周期钩子合并
4. watch
选项合并
5. props,methods, inject, computed
类似选项合并
1.6 常规选项的合并
1.6.1 el的合并
el
提供一个在页面上已存在的 DOM
元素作为 Vue
实例的挂载目标,因此它只在创建Vue
实例才存在,在子类或者子组件中无法定义el
选项,因此el
的合并策略是在保证选项只存在于根的Vue
实例的情形下使用默认策略进行合并。
strats.el = function (parent, child, vm, key) {
if (!vm) { // 只允许vue实例才拥有el属性,其他子类构造器不允许有el属性
warn(
"option \"" + key + "\" can only be used during instance " +
'creation with the `new` keyword.'
);
}
// 默认策略
return defaultStrat(parent, child)
};
1.6.2 data合并
常规选项的重点部分是在于data
的合并,读完这部分源码,可能可以解开你心中的一个疑惑,为什么data
在vue
创建实例时传递的是一个对象,而在组件内部定义时只能传递一个函数。
// data的合并
strats.data = function (parentVal, childVal, vm) {
// vm代表是否为Vue创建的实例,否则是子父类的关系
if (!vm) {
if (childVal && typeof childVal !== 'function') { // 必须保证子类的data类型是一个函数而不是一个对象
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); // vue实例时需要传递vm作为函数的第三个参数
};
data
策略最终调用的mergeDataOrFn
方法,区别在于当前vm
是否是实例,或者是单纯的子父类的关系。如果是子父类的关系,需要对data
选项进行规范校验,保证它的类型是一个函数而不是对象。
function mergeDataOrFn ( parentVal, childVal, vm ) {
// 子父类
if (!vm) {
if (!childVal) { // 子类不存在data选项,则合并结果为父类data选项
return parentVal
}
if (!parentVal) { // 父类不存在data选项,则合并结果为子类data选项
return childVal
}
return function mergedDataFn () { // data选项在父类和子类同时存在的情况下返回的是一个函数
// 子类实例和父类实例,分别将子类和父类实例中data函数执行后返回的对象传递给mergeData函数做数据合并
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// Vue实例
// vue构造函数实例对象
return function mergedInstanceDataFn () {
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
// 当实例中传递data选项时,将实例的data对象和Vm构造函数上的data属性选项合并
return mergeData(instanceData, defaultData)
} else {
// 当实例中不传递data时,默认返回Vm构造函数上的data属性选项
return defaultData
}
}
}
}
从源码的实现看,data
的合并不是简单的将两个数据对象进行合并,而是直接返回一个mergedDataFn
或者mergedInstanceDataFn
函数,而真正合并的时机是在后续初始化数据响应式系统的环节进行的,初始化数据响应式系统的第一步就是拿到合并后的数据,也就是执行mergeData
逻辑。
(关于响应式系统的构建请移步后面的章节)
function mergeData (to, from) {
if (!from) { return to }
var key, toVal, fromVal;
// Reflect.ownKeys可以拿到Symbol属性
var keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
if (!hasOwn(to, key)) {
// 子的数据父没有,则将新增的数据加入响应式系统中。
set(to, key, fromVal);
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 处理深层对象,当合并的数据为多层嵌套对象时,需要递归调用mergeData进行比较合并
mergeData(toVal, fromVal);
}
}
return to
}
mergeData
方法的两个参数是父data
选项和子data
选项的结果,也就是两个data
对象,从源码上看数据合并的原则是,将父类的数据整合到子类的数据选项中, 如若父类数据和子类数据冲突时,保留子类数据。如果对象有深层嵌套,则需要递归调用mergeData
进行数据合并。
最后回过头来思考一个问题,为什么Vue
组件的data
是一个函数,而不是一个对象呢?
我觉得可以这样解释:组件设计的目的是为了复用,每次通过函数创建相当于在一个独立的内存空间中生成一个data
的副本,这样每个组件之间的数据不会互相影响。
1.7 自带资源选项合并
在1.2中我们看到了Vue
默认会带几个选项,分别是components
组件, directive
指令, filter
过滤器,所有无论是根实例,还是父子实例,都需要和系统自带的资源选项进行合并。它的定义如下:
// 资源选项
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
// 定义资源合并的策略
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets; // 定义默认策略
});
这些资源选项的合并逻辑很简单,首先会创建一个原型指向父类资源选项的空对象,再将子类选项赋值给空对象。
// 资源选项自定义合并策略
function mergeAssets (parentVal,childVal,vm,key) {
var res = Object.create(parentVal || null); // 创建一个空对象,其原型指向父类的资源选项。
if (childVal) {
assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象
return extend(res, childVal) // 子类选项赋值给空对象
} else {
return res
}
}
结合下面的例子,我们看具体合并后的结果:
var vm = new Vue({
components: {
componentA: {}
},
directives: {
'v-boom': {}
}
})
console.log(vm.$options.components)
// 根实例的选项和资源默认选项合并后的结果
{
components: {
componentA: {},
__proto__: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
}
},
directives: {
'v-boom': {},
__proto__: {
'v-show': {},
'v-model': {}
}
}
}
简单总结一下,对于 directives、filters
以及 components
等资源选项,父类选项将以原型链的形式被处理。子类必须通过原型链才能查找并使用内置组件和内置指令。
1.8 生命周期钩子函数的合并
在学习Vue
时,有一个重要的思想,生命周期。它是我们使用Vue
高效开发组件的基础,我们可以在组件实例的不同阶段去定义需要执行的函数,让组件的功能更加丰富。在介绍生命周期钩子函数的选项合并前,我们有必要复习以下官方的生命周期图。
然而从源码中我们可以看到Vue
的生命周期钩子不止这些,它有多达12个之多,每个钩子的执行时机我们暂且不深究,它们会在以后的章节中逐一出现。我们关心的是:子父组件的生命周期钩子函数是遵循什么样的规则合并。
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook; // 对生命周期钩子选项的合并都执行mergeHook策略
});
mergeHook
是生命周期钩子合并的策略,简单的对代码进行总结,钩子函数的合并原则是:
- 如果子类和父类都拥有相同钩子选项,则将子类选项和父类选项合并。
- 如果父类不存在钩子选项,子类存在时,则以数组形式返回子类钩子选项。
- 当子类不存在钩子选项时,则以父类选项返回。
- 子父合并时,是将子类选项放在数组的末尾,这样在执行钩子时,永远是父类选项优先于子类选项执行。
// 生命周期钩子选项合并策略
function mergeHook (
parentVal,
childVal
) {
// 1.如果子类和父类都拥有钩子选项,则将子类选项和父类选项合并,
// 2.如果父类不存在钩子选项,子类存在时,则以数组形式返回子类钩子选项,
// 3.当子类不存在钩子选项时,则以父类选项返回。
var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal;
return res
? dedupeHooks(res)
: res
}
// 防止多个组件实例钩子选项相互影响
function dedupeHooks (hooks) {
var res = [];
for (var i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res
}
下面结合具体的例子看合并结果。
var Parent = Vue.extend({
mounted() {
console.log('parent')
}
})
var Child = Parent.extend({
mounted() {
console.log('child')
}
})
var vm = new Child().$mount('#app');
// 输出结果:
parent
child
简单总结一下:对于生命周期钩子选项,子类和父类相同的选项将合并成数组,这样在执行子类钩子函数时,父类钩子选项也会执行,并且父会优先于子执行。
1.9 watch选项合并
在使用Vue
进行开发时,我们有时需要自定义侦听器来响应数据的变化,当需要在数据变化时执行异步或者开销较大的操作时,watch
往往是高效的。对于 watch
选项的合并处理,它类似于生命周期钩子,只要父选项有相同的观测字段,则和子的选项合并为数组,在监测字段改变时同时执行父类选项的监听代码。处理方式和生命钩子选项的区别在于,生命周期钩子选项必须是函数,而watch
选项最终在合并的数组中可以是包含选项的对象,也可以是对应的回调函数,或者方法名。
strats.watch = function (parentVal,childVal,vm,key) {
//火狐浏览器在Object的原型上拥有watch方法,这里对这一现象做了兼容
// var nativeWatch = ({}).watch;
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
// 没有子,则默认用父选项
if (!childVal) { return Object.create(parentVal || null) }
{
// 保证watch选项是一个对象
assertObjectType(key, childVal, vm);
}
// 没有父则直接用子选项
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
// 父的选项先转换成数组
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
下面结合具体的例子看合并结果:
var Parent = Vue.extend({
watch: {
'test': function() {
console.log('parent change')
}
}
})
var Child = Parent.extend({
watch: {
'test': {
handler: function() {
console.log('child change')
}
}
},
data() {
return {
test: 1
}
}
})
var vm = new Child().$mount('#app');
vm.test = 2;
// 输出结果
parent change
child change
简单总结一下:对于watch选项的合并,最终和父类选项合并成数组,并且数组的选项成员,可以是回调函数,选项对象,或者函数名。
1.10 props methods inject computed合并
源码的设计将props.methods,inject,computed
归结为一类,他们的配置策略一致,简单概括就是,如果父类不存在选项,则返回子类选项,子类父类都存在时,用子类选项去覆盖父类选项。
// 其他选项合并策略
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal,childVal,vm,key) {
if (childVal && "development" !== 'production') {
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal } // 父类不存在该选项,则返回子类的选项
var ret = Object.create(null);
extend(ret, parentVal); //
if (childVal) {
// 子类选项会覆盖父类选项的值
extend(ret, childVal); }
return ret
};
1.11 小结
至此,五类选项合并的策略分析到此结束,回顾一下这一章节的内容,这一节是Vue
源码分析的起手式,所以我们从Vue
的引入出发,先大致了解了Vue
在代码引入阶段做的操作,主要是对静态属性方法和原型上属性方法的定义和声明,这里并不需要精确了解到每个方法的功能和实现细节,当然我也相信你已经在实战中或多或少接触过这些方法的使用。接下来到文章的重点,new Vue
是我们正确使用Vue
进行开发的关键,而实例化阶段会对调用_init
方法进行初始化,选项合并是初始化的第一步。选项合并会对系统内部定义的选项和子父类的选项进行合并。而Vue
有相当丰富的选项合并策略,不管是内部的选项还是用户自定义的选项,他们都遵循内部约定好的合并策略。有了丰富的选项和严格的合并策略,Vue
在指导开发上才显得更加完备。下一节会分析一个重要的概念,数据代理,它也是响应式系统的基础。
- 深入剖析Vue源码 - 选项合并(上)
- 深入剖析Vue源码 - 选项合并(下)
- 深入剖析Vue源码 - 数据代理,关联子父组件
- 深入剖析Vue源码 - 实例挂载,编译流程
- 深入剖析Vue源码 - 完整渲染过程
- 深入剖析Vue源码 - 组件基础
- 深入剖析Vue源码 - 组件进阶
- 深入剖析Vue源码 - 响应式系统构建(上)
- 深入剖析Vue源码 - 响应式系统构建(中)
- 深入剖析Vue源码 - 响应式系统构建(下)
- 深入剖析Vue源码 - 来,跟我一起实现diff算法!
- 深入剖析Vue源码 - 揭秘Vue的事件机制
- 深入剖析Vue源码 - Vue插槽,你想了解的都在这里!
- 深入剖析Vue源码 - 你了解v-model的语法糖吗?
- 深入剖析Vue源码 - Vue动态组件的概念,你会乱吗?
- 彻底搞懂Vue中keep-alive的魔法(上)
- 彻底搞懂Vue中keep-alive的魔法(下)