上一章的代码规范化了各个选项的值,主要是为了接下来更好的进行合并选项。
下面开始就是真正进行选项合并的代码。
具体要怎么合并呢?不同的字段的合并策略是一样的吗?
mergeField#
继续看最后剩下的代码,这里开始进行真正的选项合并。
const options = {};
let key;
// ! 遍历默认配置
for (key in parent) {
mergeField(key);
}
// ! 遍历用户配置
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
// ! 根据 key 值的不同,使用不同的合并策略
function mergeField(key) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;首先声明一个变量options为空对象,最后再返回它,这说明选项合并后的值是对象类型。然后遍历 Vue 的默认选项parent,调用mergeField函数并传入key值。再遍历用户选项child,还是使用mergeField函数并传入key值,但是和上面不同的是要求key必须是parent中没有的字段。
为什么要这样设计呢?看mergeField函数就知道了。
在使用策略函数合并时都会把值传入,所以不需要重复调用。
mergeField函数是合并选项字段的方法,首先根据key的值选择它的专用的合并策略,如果没有专用的合并策略就使用默认的合并策略。
这里的strats是一个合并策略函数组成的对象,对象里面的key对应选项字段中的key,而它的value对应的是字段的合并策略函数。再调用策略函数的时候,需要把parent字段的值、child选项字段的值以及实例对象vm和key作为参数一起传入。然后把调用的结果(即合并后的选项)赋值给一开始声明的options对象中。options对象中的key也对应选项字段的key,它的值就是合并选项后的值。
这就是合并选项的逻辑,比较简单,就是根据选项中不同的字段选择不同的合并策略。不过难点并不在这里,而是在合并选项的策略函数上。
下面来看下各个字段的合并策略函数。
strats#
defaultStrat#
先看下默认的合并策略defaultStrat函数的代码。在mergeField函数中,我们知道如果strats中没有找到key的合并策略,就会使用默认的合并策略函数。
代码在core/util/options.js文件中。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal;
};它的逻辑比较简单,如果没有用户选项childVal,就使用默认选项parentVal,否则使用用户选项childVal。
使用默认合并策略的字段有el和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: '#app',
propsData: {
name: 'Hale'
}
// 用户选项
el: '#root'
// 合并后选项
el: '#root',
propsData: {
name: 'Hale'
}data选项的合并策略#
接下来看下data字段的合并策略函数。
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);
};函数逻辑分为两种情况,有实例对象vm和没有vm。
当我们使用Vue.extend方法生成子类时,这个子类是一个构造函数,当它的options作为用户选项childVal时,它是没有vm的。
不过不管有没有实例对象,它都是使用mergeDataOrFn函数来进行选项合并的。
另外,子类组件的data选项如果不是函数类型,在非生产环境时会报错,然后直接返回parentVal选项。
mergeDataOrFn
下面看下mergeDataOrFn函数的代码,这里的逻辑同样分为两种情况,分别为有实例对象vm和没有vm。
先看下没有实例对象vm的代码,即合并子类的选项。
export function mergeDataOrFn(
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (!childVal) {
return parentVal;
}
if (!parentVal) {
return childVal;
}
return function mergedDataFn() {
return mergeData(
typeof childVal === "function" ? childVal.call(this, this) : childVal,
typeof parentVal === "function" ? parentVal.call(this, this) : parentVal
);
};
} else {
// ...
}
}首先是两个if语句,先看第一个。如果没有子选项childVal,就返回父选项parentVal。比如我们在扩展时没有设置data选项的值。像下面这种情况,就会采用这种逻辑。
const Stor = Vue.extend({});可能有人会问既然子选项childVal中没有设置data选项,父选项parentVal通常也不会有data的值,都没有data这个字段,也就不存在去调用data的策略函数了。
为什么还要返回parentVal,parentVal可能存在吗?
答案是肯定的,如下示例代码中,在使用二次extend时,Child子类的parentVal是存在的,就是在创建Parent子类时传入的data的值。但是在创建Child子类时没有传入data的值,所以Child的childVal是没有的。那么对于Child的子类的创建的函数,存在parentVal而不存在childVal。
const Parent = Vue.extend({
data() {
return { test1: 1 };
},
});
const Child = Parent.extend({});接下来再看第二个if语句。如果没有parentVal,就返回childVal。其实这种情况在实际开发中应该是出现最多的。所以parentVal和childVal必定有其中一个,否则就不会执行data选项合并策略了。
接下来查看parentVal和childVal都存在的情况的逻辑代码。
return function mergedDataFn() {
return mergeData(
typeof childVal === "function" ? childVal.call(this, this) : childVal,
typeof parentVal === "function" ? parentVal.call(this, this) : parentVal
);
};这里返回一个函数mergedDataFn,这个函数调用了mergeData来处理data选项的合并,它需要传入childVal和parentVal这两个参数。注意参数传入的顺序,先子后父。另外传入的两个参数只能是对象类型。
在 Vue 实例组件中data的值都是函数类型的,它是一个返回普通对象的函数。这时需要调用这个函数,输出对象类型的值。在使用call调用的时候传入了两个this。第一个this是指向data函数的作用域。而第二个this是data函数的参数,指向生成的实例对象。
接下来看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的情况,也就是说上面代码是实例对象初始化时合并选项的代码。
这样也返回一个叫mergedInstanceDataFn函数。函数中需要先获取一个childVal和parentVal生成的对象类型的值。如果选项是函数,需要调用这个函数获取对象类型的值,在调用时传入的参数是两个vm,即实例对象。第一个vm表示this的指向是实例对象,第二个vm是data函数的参数。
在组件中,data是一个函数,它其实有很多种写法。
data () {
return {
testData: this.propsData
}
}
// 另外的写法
data(vm) {
return {
testData: vm.propsData
}
}
// 还可以使用解构
data({ propsData }) {
return {
testData: propsData
}
}继续看下面的代码。
if (instanceData) {
return mergeData(instanceData, defaultData);
} else {
return defaultData;
}判端childVal输出的值是否存在,如果存在,就调用mergeData函数把它和parentVal输出的值进行合并,不存在就直接返回parentVal输出的值。
下面看下mergeData函数的代码。
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;
}mergeData函数接收两个参数,to和from,从参数名中可以看出是把第二个参数合并到第一个参数中。
然后再看函数的返回值为to,即返第一个参数合并的值,所以为什么我们在上面调用它的时候传入参数的顺序是先子后父了,即先传入childVal再传入parentVal,因为是以用户选项childVal为主。
看代码第一句if (!from) return to,如果没有第二个参数from,直接返回第一个参数to。
然后获取第二个参数from的keys,判断是否支持 ES6 的Promise的语法,如果支持则使用Reflect.ownKeys(from),不支持就使用对象的方法Object.keys(from)。
它们两个有什么区别吗?使用Object.keys只会返回可枚举的key值,而Reflect.ownKeys会返回所有的key值。
然后去遍历keys,如果key是'__ob__时,这是侦听对象(后面讲响应式系统时再说明),跳过这次循环。
在看下to选项是否有key这个值,如果没有的话,使用set方法把它加入到to中。这里的set和全局 APIVue.set是同一个函数,可以把data选项的属性变成响应式。
另外,还需要判断,在相同key时,to和from的值是否相同,如果不相同且它们都是普通对象时,递归调用mergeData函数进行深度合并。
小结,mergeData合并策略是以to选项为主。对于from选项,只有to选项中不存在的key值,才会被添加到to选项中。如果存在相同的key时,当它的值是原型类型时,选取to的值,即to的值覆盖掉from的值。当它的值是对象类型时,使用递归进行深度合并。
另外还有一点非常重要,那就是合并后的data选项还是一个函数。
示例代码
// from
data() {
return {
age: 20,
name: 'Hale',
friends: {
Beijing: 'xiaoming',
Shenzhen: 'xiaohong'
}
}
}
// to
data() {
return {
age: 18,
friends: {
Beijing: 'Jack',
Shanghai: 'Amy'
}
}
}
// after merge
data() {
return {
age: 18,
name: 'Hale',
friends: {
Beijing: 'Jack',
Shanghai: 'Amy',
Shenzhen: 'xiaohong'
}
}
}mergeHook生命周期函数的合并策略#
下面看下生命周期钩子函数的合并策略。
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;
}先看函数的参数,parentVal是一个由函数组成的数组,而childVal可能是函数或者是由函数组成的数组,它们都是可选的。
再看代码,没有使用if语句,而是使用了三组三元操作符。
首先判断是否有childVal,没有的话就返回parentVal。
有childVal的话,再判断是否有parentVal,如果有parentVal,就把childVal合并到parentVal中组成新的数组。如果没有parentVal,判断childVal的类型是否是数组,如果是数组,就直接返回它本身。如果不是数组,就把它包装成数组并返回。总之合并后的res一定是个数组。
最后输出结果是还需要调用dedupeHooks函数去除重复的钩子。函数遍历里数组里面的函数,如果多个函数来自同一个引用地址,则会把其它相同引用的函数排除掉。
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;
}从上面的合并策略中可以得知:在组件实例中,创建的钩子可以是函数,也可以是多个函数组成的数组。
小结,mergeHook合并策略是把同类的所有的钩子都合并放到一个数组中。
但是官方文档中并没有说明这种情况,但是经过实践,是可以这么做的,如下面的代码,输出按照顺序指向函数的打印结构。
export default {
created: [
function fn1() {
console.log("created hook fn1");
},
function fn2() {
console.log("created hook fn2");
},
function fn3() {
console.log("created hook fn3");
},
],
};继续看下面代码,使用生命周期函数合并策略的字段那肯定就是生命周期钩子,比如beforeCreate、mounted等。
LIFECYCLE_HOOKS.forEach((hook) => {
strats[hook] = mergeHook;
});
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
"activated",
"deactivated",
"errorCaptured",
"serverPrefetch",
];示例代码
// parentVal
created: [function f1() {}, function f2() {}],
// childVal
created: function f3() {
console.log('f3')
}
// after merge
created: [
function f1() {},
function f2() {},
function f3() {
console.log('f3')
}
]mergeAssets资源选项的合并策略#
在 Vue 的实例选项中,components、directives、filters被认为是资源。
为什么呢?因为这些选项都是可以作为第三方库来提供的。
资源选项的合并策略的函数是mergeAssets,下面看下它的代码。
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;
}
}如果有parentVal则以它作为原型对象创建对象res,没有的话以null为原型对象创建对象res。
然后判断是否有childVal,如果有的话使用extend把它扩展到res中。如果没有的话,直接返回res。
另外,有childVal且在非生产环境时,会判断childVal的对象中的值是不是一个普通对象,如果不是会报错。
小结,mergeAssets合并策略先创建以parentVal为原型创建对象res,然后再使用extend函数去扩展childVal,这样如果存在相同的key时,childVal选项就不会覆盖掉parentVal选项的值。因为parentVal的是值保存在res的隐式原型对象__proto__字段中。
extend函数代码如下:
export function extend(to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key];
}
return to;
}使用资源选项的合并策略的字段当然就是我们上面提到的资源选项components、directives、filters。
ASSET_TYPES.forEach(function (type) {
strats[type + "s"] = mergeAssets;
});
export const ASSET_TYPES = ["component", "directive", "filter"];示例代码
// parentVal
components: {
CompA,
CompB
}
// childVal
components: {
CompC
}
// after merge
components: {
CompC,
__proto__: {
CompA,
CompB,
}
}watch选项的合并策略#
下面查看watch选项的合并策略的代码,代码比较长,一段一段分析
首先看最前面的两行代码。
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;
// ...
};首先判断,这两个选项是否全等于nativeWatch,它是个什么呢?看下面的代码。
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = {}.watch;通过注释和查阅资料得知,Firefox 浏览器会给每个对象的原型中添加watch属性,这样就和 Vue 里面的watch属性名称产生冲突。所以需要判断一下,当 Vue 的watch属性和 FireFox 浏览器定义的watch的值相同时,会把watch设置为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;如果不存在childVal,则返回以parentVal或者null为原型创建的对象。
在非生产环境中,还会判断下childVal的值是否是一个普通对象,如果不是会报错。
如果不存在parentVal,则直接返回childVal。
如果childVal和parentVal都存在,继续看下面的代码。
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;先什么一个变量ret存储函数的返回值。然后把parentVal扩展到ret对象中,这样ret就拥有parentVal的全部属性。
然后遍历childVal对象,声明一个变量parent存储ret[key]的值,注意这个值可能不存在。声明变量child存储childVal[key]的值,这个值是一定存在的。
判断如果parent存在且不是数组时,把parent包装成一个数组。最后开始给ret赋值,它的key就是每次遍历childVal的key。在每次遍历的过程中,如果存在parent就把child合并到parent中组成新的数组。
如果不存在parent,判断child是不是一个数组,如果是的话,就直接返回它本身。如果不是的话,把它包装成数组并返回。最后返回ret这个对象。
总结,watch的合并策略和其他的策略不同,它不会去除相同key的值,而是把它们的值组合在一起。也就是说如果有多个监听同一个值的函数,那么当这个值变化时,所有的监听函数都会生效。
示例代码
// parentVal
watch: {
test: function() {
console.log('test parentVal')
}
}
// childVal
watch: {
test: function() {
console.log('test childVal')
}
}
// after merge
watch: {
test: [
function() {
console.log('test parentVal')
},
function() {
console.log('test childVal')
}
]
}
// test change
vm.test = 2
// test parentVal
// test childValprops、methods、inject、computed选项的合并策略#
看下面一段代码。
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;
};这是对props、methods、inject、computed字段的合并选项的策略。
代码逻辑比较简单。首先当childVal存在且在非生产环境中时,会判断childVal的值是否为普通对象,如果不是会报错。然后判断parentVal是否存在,如果不存在,则返回childVal。
再声明一个没有原型的对象ret,使用parentVal扩展它。
最后判断如果有childVal,也把childVal扩展到ret中。如果没有的话,直接返回ret。
小结,这个合并策略就是很简单的扩展,先创建一个没有原型的对象ret,先使用parentVal扩展给它,然后再使用childVal扩展它。如果选项中存在相同的key,后面的值会覆盖掉前面的值。
示例代码
// parentVal
props: {
name: {
type: String,
default: 'Hale'
}
},
methods: {
fn1() {
console.log('parentVal fn1')
}
}
// childVal
props: {
name: {
type: String,
default: 'Amy'
}
},
methods: {
fn2() {
console.log('childVal fn2')
}
}
// after merge
props: {
name: {
type: String,
default: 'Amy'
}
},
methods: {
fn1() {
console.log('parentVal fn1')
},
fn2() {
console.log('childVal fn2')
}
}provide选项的合并策略#
看下provide选项的合并策略的代码。
strats.provide = mergeDataOrFn;provide选项沿用了data选项的一样的合并策略,这里就不多赘述了。
extends和mixins的合并策略#
看下extends和mixins的合并策略。
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm); // ! 合并 extends
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm); // ! 合并 mixins
}
}
}学习完所有的合并策略后,再看extends和mixins的合并策略,这里在前面规范化的时候已经讲解过了。
我们都知道extends和mixins选项的作用是复用组件代码,那么选项里面的值就可能是上面出现的所有选项字段,所以需要使用递归进行深度合并,然后再次沿用上面的合并策略。
合并策略总结#
所有的合并策略已经已经讲解完毕,下面进行总结。
- 对于
el、propsData选项使用默认的合并策略defaultStrat。 - 对于
data选项,使用mergeDataOrFn函数进行处理,最终结果是data选项将变成一个新的函数,这个函数返回mergeData函数处理的数据,mergeData是真正合并选项的函数。 - 对于
生命周期钩子选项,将合并成数组,使得父子选项中的钩子函数都能够被执行。 - 对于
directives、filters和components资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。 - 对于
watch选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的字段,把它们合并为数组,这样所有的观察者都将被执行。 - 对于
props、methods、inject和computed选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。 - 对于
provide选项,其合并策略使用与data选项相同的mergeDataOrFn函数。 - 最后,以上没有提及到的选项都将使默认选项
defaultStrat,只要子选项不是undefined就使用子选项,否则使用父选项。
optionMergeStrategies自定义合并策略#
Vue 在全局配置中还提供了一个可以自定义合并策略的选项optionMergeStrategies,让用户可以为一些非系统选项字段提供自定义的合并策略,而不是使用默认的合并策略。
比如为自定义字段customOption的提供自定义的合并策略``optionMergeStrategies`。
示例代码如下,现在在入口文件main.js中配置合并策略。
Vue.config.optionMergeStrategies.customOption = function (parent, child, vm) {
return parent ? parent + child : child;
};然后在实例组件中使用这个自定义的字段customOption。
export default {
customOption: 2,
extends: {
customOption: 1,
},
data() {
return {
a: 0,
};
},
created() {
console.log(this.$options.customOption); // 3
},
};$options合并后选项的使用#
使用mergeOptions后会返回一个对象,这个对象就是合并后的选项,在实例化中会把这个对象赋值给实例的$options属性。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // ! Vue 初始化时的默认选项,比如默认的指令和组件等等
options || {}, // ! 用户传入的配置
vm // ! Vue 实例对象本身
);vm.$options这个属性非常重要,在接下来设置其他的实例属性时至关重要,因为都会用到它。
在实例组件中也可以调用这个属性去拿到合并后的选项,包括我们自己输入的选项,这样我们可以更好的编写组件。
export default {
name: 'Home',
data() {
return {
a: 0
}
},
methods: {
test() {
console.log('test method')
}
},
created() {
console.log(this.$options.name) // 'Home'
console.log(this.$options.methods.test) // function test() { console.log('test method') }
}