vue源码分析(十六) 选项合并之合并策略
# 1. 概述
上一章我们分析了 Vue
选项合并中的规范化,在这一章中我们重点来看看Vue
选项合并中的合并策略。
在分析之前,我们还是以上一章的案例进行讲解,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue 源码分析</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// Vue.config.devtools = true
// Vue.config.performance = true
new Vue({
el: '#app',
template: `
<div> {{ name }} </div>
`,
data: {
name: 'robin'
}
})
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
我们继续往下看,接下来是一段如下的代码:
源码目录:src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
/*省略...*/
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
/*省略...*/
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在上一章 (opens new window)中我们分析过了参数 parent
和 child
的值在我们当前案例中,这两个参数分别为,如下:
// parent
parent = {
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
// child
child = {
el: "#app",
template: "<div> {{ name }} </div>",
data: {
name: "robin"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
知道了parent
和 child
的参数格式,我们继续看看 mergeOptions
函数中的这段代码,这段代码的作用是处理 extends
选项和 mixins
选项。首先判断我们传进来的 options
即 child
是否存在 _base
属性,如果不存在继续执行后面的语句, 然后判断 child.extends
是否存在,如果存在的话就递归调用 mergeOptions
函数将 parent
与 child.extends
进行合并,并将结果作为新的 parent
。
接着检测 child.mixins
选项是否存在,如果存在则使用同样的方式进行操作,不同的是,由于 mixins
是一个数组所以要遍历一下。
说明:由于处理 extends
和 mixins
是通过递归调用mergeOptions
函数,所有我们在分析完 mergeOptions
以后在来看看这两个处理逻辑。
我们继续往下看,代码如下:
源码目录:src/core/util/options.js
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
/*省略...*/
return options
2
3
4
5
6
7
8
9
10
11
12
这段代码有两个 for
循环,分别是对 parent
和 child
的处理,我们分别来分析一下:
- 第一个
for...in
循环
这个 for... in
用来遍历 parent
,并且将 parent
对象的键作为参数传递给 mergeField
函数。前面我们已经知道 parent
对象包含 components
、directives
、filters
以及 _base
四个属性。
- 第二个
for...in
循环
这个 for... in
用来遍历 child
,并且将 child
对象的键作为参数传递给 mergeField
函数。前面我们已经知道 child
对象包含 el
、templates
、data
三个属性。
child
的 for... in
循环中唯一和 parent
区别是,如果 child
对象的键也在 parent
上出现,那么就不要再调用 mergeField
了,因为在上一个 for in
循环中已经调用过了,这就避免了重复调用。
此处的 hasOwn
作用是用来判断一个属性是否是对象自身的属性(不包括原型上的)。
接下来我们继续看看 mergeField
的定义,如下:
源码目录:src/core/util/options.js
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
2
3
4
mergeField
逻辑很简单,首先通过传进来的 key
在 strats
对象上获取次 key
对应的值赋值给变量 strat
,如果不存在则使用默认的 defaultStrat
赋值给变量 strat
。
接下来通过调用 strat
引用的函数以此 key
在parent
和 child
对应的值、vm
实例、key
作为参数处理,将返回的结果赋值给此 key
在 options
对象上对应的属性。
# 2. 合并策略
上一小节我们分析了 mergeOptions
,我们知道合并选项最终是通过各自的合并策略完成的,下面我们就分析一下所有的合并策略。
我们通过调试工具,查看到的 strats
对象最终的结构,如下:
strats = {
el: function (parent, child, vm, key) { /*defaultStrat...*/ },
propsData: function (parent, child, vm, key) { /*defaultStrat...*/ },
data: function ( parentVal, childVal, vm ) { /*mergeDataOrFn...*/ },
beforeCreate: function mergeHook( parentVal, childVal ) { /*...*/ },
created: function mergeHook( parentVal, childVal ) { /*...*/ },
beforeMount: function mergeHook( parentVal, childVal ) { /*...*/ } ,
mounted: function mergeHook( parentVal, childVal ) { /*...*/ },
beforeUpdate: function mergeHook( parentVal, childVal ) { /*...*/ },
updated: function mergeHook( parentVal, childVal ) { /*...*/ },
beforeDestroy: function mergeHook( parentVal, childVal ) { /*...*/ },
destroyed: function mergeHook( parentVal, childVal ) { /*...*/ },
activated: function mergeHook( parentVal, childVal ) { /*...*/ },
deactivated: function mergeHook( parentVal, childVal ) { /*...*/ },
errorCaptured: function mergeHook( parentVal, childVal ) { /*...*/ },
serverPrefetch: function mergeHook( parentVal, childVal ) { /*...*/ },
components: function mergeAssets( parentVal, childVal, vm, key ) { /*...*/ },
directives: function mergeAssets( parentVal, childVal, vm, key ) { /*...*/ },
filters: function mergeAssets( parentVal, childVal, vm, key ) { /*...*/ },
watch: function ( parentVal, childVal, vm, key ) { /*...*/ },
props: function ( parentVal, childVal, vm, key ) { /*...*/ },
methods: function ( parentVal, childVal, vm, key ) { /*...*/ },
inject: function ( parentVal, childVal, vm, key ) { /*...*/ },
computed: function ( parentVal, childVal, vm, key ) { /*...*/ },
provide: function mergeDataOrFn( parentVal, childVal, vm ) { /*...*/ }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 2.1 选项el & propsData
首先我们来看一下 el & propsData
的合并策略,如下:
源码目录:src/core/util/options.js
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)
}
}
2
3
4
5
6
7
8
9
10
11
在非生产环境下,给 strats
对象上添加 el
和 propsData
属性,值为一个匿名函数。这个匿名函数是用来合并 el
选项和 propsData
选项的。
我们再来看看匿名函数内部的逻辑,首先判断传递 vm
不存在的话直接报一个警告,如果存在则调用 defaultStrat
进行处理。此处需要注意这个警告的内容,表示在父组件中即使用new Vue
创建的实例中使用 el
或 propsData
选项,是没有什么问题,但是在子组件中也使用了 el
或 propsData
选项,这就会得到如上警告。
如下声明,就会报出上面的警告:
// 子组件
const child = {
el: '#child',
template: `<div> {{ name }} </div>`,
data() {
return { name: 'robin' }
}
}
// 父组件
new Vue({
el: '#app',
components: {child},
template: `<child/>`,
data: {
name: 'robin'
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
策略函数中的 vm
来自于 mergeOptions
函数的第三个参数。所以当调用 mergeOptions
函数且不传递第三个参数的时候,那么在策略函数中就拿不到 vm
参数。所以我们可以猜测到一件事,那就是 mergeOptions
函数除了在 _init
方法中被调用之外,还在 Vue.extend
方法中被调用的,此时调用 mergeOptions
函数就没有传递第三个参数,也就是说通过 Vue.extend
创建子类的时候 mergeOptions
会被调用,此时策略函数就拿不到第三个参数。
在策略函数中通过判断是否存在 vm
就能够得知 mergeOptions
是在实例化时调用(使用 new
操作符走 _init
方法)还是在继承时调用(Vue.extend
),而子组件的实现方式就是通过实例化子类完成的,子类又是通过 Vue.extend
创造出来的,所以我们就能通过对 vm
的判断而得知是否是子组件了。
所以最终的结论就是:如果策略函数中拿不到 vm
参数,那么处理的就是子组件的选项。
接下来我们回到策略函数中,来分析 defaultStrat
,定义如下:
源码目录:src/core/util/options.js
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
2
3
4
5
它的逻辑很简单,子选项不是 undefined
那么就是用子选项,否则使用父选项。
但是大家还要注意一点,strats.el
和 strats.propsData
这两个策略函数是只有在非生产环境才有的,在生产环境下访问这两个函数将会得到 undefined
,那这个时候 mergeField
函数的第一句代码就起作用了:
// 当一个选项没有对应的策略函数时,使用默认策略
const strat = strats[key] || defaultStrat
2
所以在生产环境将直接使用默认的策略函数 defaultStrat
来处理 el
和 propsData
这两个选项。
# 2.2 选项 data
我们再来看看选项 data
的合并策略,如下:
源码目录:src/core/util/options.js
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)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这段代码首先在 strats
对象上添加一个 data
属性,其值是一个函数。接下来我们看看,这个函数的内容,函数内部与 el
和 propsData
这两个策略函数相同,先判断是否传递了 vm
这个参数,我们知道当没有 vm
参数时,说明处理的是子组件的选项,那我们就看看对于子组件的选项它是如何处理的。
首先判断是否传递了子组件的 data
选项(即:childVal
),并且检测 childVal
的类型是不是 function
,如果 childVal
的类型不是 function
则会给你一个警告,也就是说 childVal
应该是一个函数,如果不是函数会提示你 data
的类型必须是一个函数,这就是我们知道的:子组件中的 data
必须是一个返回对象的函数。如果不是函数,除了给你一段警告之外,会直接返回 parentVal
。
如果 childVal
是函数类型,那说明满足了子组件的 data
选项需要是一个函数的要求,那么就直接返回 mergeDataOrFn
函数的执行结果。
接下来如果 vm
存在说明处理的是使用 new
操作符创建实例时的选项,这个时候则直接返回 mergeDataOrFn
的函数执行结果。
我们注意到了在选项 data
处理策略中,不管是子组件还是使用 new
操作符创建实例的选项,最终都是调用 mergeDataOrFn
函数,不同的是在处理使用 new
操作符创建实例的选项时,多传递了第三个参数 vm
。那么问题来了为什么处理使用 new
操作符创建实例的选项要传递 vm
?
接下我们带着这个问题再来分析 mergeDataOrFn
的源码,如下:
源码目录:src/core/util/options.js
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
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
我们在前面的分析知道,当 vm
存在时说明处理的是非子组件,当 vm
不存时处理的是子组件,在处理子组件和非子组件时都调用了 mergeDataOrFn
函数,只是传递的参数不同,所以我们分析到这里知道前面我们提出的问题的答案了。mergeDataOrFn
中通过 vm
存在与否来判断处理的非子组件还是子组件。
vm
不存在说明处理的是子组件,执行 if
逻辑,vm
存在说明处理的是非子组件,执行 else
逻辑。
我们继续往下看,在处理子组件的逻辑中,第一个 if
语句块,如果没有 childVal
,也就是说子组件的选项中没有 data
选项,那么直接返回 parentVal
,比如下面的代码:
Vue.extend({})
我们使用 Vue.extend
函数创建子类的时候传递的子组件选项是一个空对象,即没有 data
选项,那么此时 parentVal
实际上就是 Vue.options
,由于 Vue.options
上也没有 data
这个属性,所以压根就不会执行 strats.data
策略函数,也就更不会执行 mergeDataOrFn
函数,有的同学可能会问:既然都没有执行,那么这里的 return parentVal
是不是多余的?当然不多余,因为 parentVal
存在有值的情况。那么什么时候才会出现 childVal
不存在但是 parentVal
存在的情况呢?看下面的代码:
const Parent = Vue.extend({
data: function () {
return {
test: 1
}
}
})
const Child = Parent.extend({})
2
3
4
5
6
7
8
9
上面的代码中 Parent
类继承了 Vue
,而 Child
又继承了 Parent
,关键就在于我们使用 Parent.extend
创建 Child
子类的时候,对于 Child
类来讲,childVal
不存在,因为我们没有传递 data
选项,但是 parentVal
存在,即 Parent.options
下的 data
选项,那么 Parent.options
是哪里来的呢?实际就是 Vue.extend
函数内使用 mergeOptions
生成的,所以此时 parentVal
必定是个函数,因为 strats.data
策略函数在处理 data
选项后返回的始终是一个函数。
由于 childVal
和 parentVal
必定会有其一,否则便不会执行 strats.data
策略函数,所以上面判断的意思就是:如果没有子选项则使用父选项,没有父选项就直接使用子选项,且这两个选项都能保证是函数,如果父子选项同时存在,则代码继续进行。
接下当父子选项同时存在,那么就返回一个函数 mergedDataFn
,注意:此时代码运行就结束了,因为函数已经返回了(return
),至于 mergedDataFn
函数里面又返回了 mergeData
函数的执行结果这句代码目前还没有执行。
以上就是 strats.data
策略函数在处理子组件的 data
选项时所做的事,我们可以发现 mergeDataOrFn
函数在处理子组件选项时返回的总是一个函数,这也就间接导致 strats.data
策略函数在处理子组件选项时返回的也总是一个函数。
说完了处理子组件选项的情况,我们再看看处理非子组件选项的情况,也就是使用 new
操作符创建实例时的情况,执行的是 else
分支,它也返回的是一个函数 mergedInstanceDataFn
,注意此时的 mergedInstanceDataFn
函数同样还没有执行,它是 mergeDataOrFn
函数的返回值,所以这再次说明了一个问题:mergeDataOrFn
函数永远返回一个函数。
通过上面的分析我们可以知道 data
选项最终被 mergeOptions
函数处理成了一个函数,当合并处理的是子组件的选项时 data
函数可能是以下三者之一:
- 1、就是
data
本身,因为子组件的data
选项本身就是一个函数,即如下mergeDataOrFn
函数的代码段所示:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// 返回子组件的 data 选项本身
if (!parentVal) {
return childVal
}
...
} else {
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 2、父类的
data
选项,如下代码段所示::
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// 返回父类的 data 选项
if (!childVal) {
return parentVal
}
...
} else {
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 3、
mergedDataFn
函数,如下代码段所示:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// 返回 mergedDataFn 函数
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当合并处理的是非子组件的选项时 data
函数为 mergedInstanceDataFn
函数,如下代码段所示:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
} else {
// 当合并处理的是非子组件的选项时 `data` 函数为 `mergedInstanceDataFn` 函数
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
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
所以这就是我们一直强调的:data
选项最终被处理为一个函数。但是根据我们之前的分析可知,函数分几种情况,但它们都有一个共同的特点,即:这些函数的执行结果就是最终的数据。
我们可以发现 mergedDataFn
和 mergedInstanceDataFn
这两个函数有一个共同的特点,内部都调用了 mergeData
处理数据并返回,我们先看一下 mergedDataFn
函数,其源码如下:
源码目录:src/core/util/options.js
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
2
3
4
5
6
这个函数直接返回了 mergeData
函数的执行结果,再看看 mergedInstanceDataFn
函数,其源码如下:
源码目录:src/core/util/options.js
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) { // childVal存在
return mergeData(instanceData, defaultData)
} else { // childVal不存在,返回parentVal
return defaultData
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我们知道 childVal
要么是子组件的选项,要么是使用 new
操作符创建实例时的选项,无论是哪一种,总之 childVal
要么是函数,要么就是一个纯对象。所以如果是函数的话就通过执行该函数从而获取到一个纯对象,所以类似上面那段代码中判断 childVal
和 parentVal
的类型是否是函数的目的只有一个,获取数据对象(纯对象)。所以 mergedDataFn
和 mergedInstanceDataFn
函数内部调用 mergeData
方法时传递的两个参数就是两个纯对象(当然你可以简单的理解为两个JSON对象)。
那么接下来我们在看看mergeData
的源码,如下:
源码目录:src/core/util/options.js
function mergeData (to: Object, from: ?Object): Object {
// 没有 from 直接返回 to
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
// 遍历 from 的 key
for (let i = 0; i < keys.length; i++) {
key = keys[i] // 获取对象的key
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key] // 获取key对应的目标对象的值
fromVal = from[key] // 获取key对应的来源对象的值
if (!hasOwn(to, key)) { // 如果 from 对象中的 key 不在 to 对象中,则使用 set 函数为 to 对象设置 key 及相应的值
set(to, key, fromVal)
} else if ( // 如果 from 对象中的 key 也在 to 对象中,且这两个属性的值都是纯对象则递归进行深度合并
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 深层递归
mergeData(toVal, fromVal)
}
// 其他情况什么都不做
}
return to
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
如果没有 from
则直接返回 to
,也就是说如果没有 parentVal
产生的值,就直接使用 childVal
产生的值。
如果有 parentVal
产生的值,则代码继续向下运行,我们看 mergeData
最后的返回值仍是 to
对象,所以你应该能猜的到 mergeData
函数的作用,可以简单理解为:将 from
对象的属性混合到 to
对象中,也可以说是将 parentVal
对象的属性混合到 childVal
中,最后返回的是处理后的 childVal
对象。
mergeData
的具体做法就是像上面 mergeData
函数的代码段中所注释的那样,对 from
对象的 key
进行遍历:
- 如果
from
对象中的key
不在to
对象中,则使用set
函数为to
对象设置key
及相应的值。 - 如果
from
对象中的key
在to
对象中,且这两个属性的值都是纯对象则递归地调用mergeData
函数进行深度合并。 - 其他情况不做处理。
一、为什么最终 strats.data
会被处理成一个函数?
这是因为,通过函数返回数据对象,保证了每个组件实例都有一个唯一的数据副本,避免了组件间数据互相影响。后面讲到 Vue
的初始化的时候大家会看到,在初始化数据状态的时候,就是通过执行 strats.data
函数来获取数据并对其进行处理的。
二、为什么不在合并阶段就把数据合并好,而是要等到初始化的时候再合并数据?
这个问题是什么意思呢?我们知道在合并阶段 strats.data
将被处理成一个函数,但是这个函数并没有被执行,而是到了后面初始化的阶段才执行的,这个时候才会调用 mergeData
对数据进行合并处理,那这么做的目的是什么呢?
其实这么做是有原因的,后面讲到 Vue
的初始化的时候,大家就会发现 inject
和 props
这两个选项的初始化是先于 data
选项的,这就保证了我们能够使用 props
初始化 data
中的数据,如下:
// 子组件:使用 props 初始化子组件的 childData
const Child = {
template: '<span></span>',
data () {
return {
childData: this.parentData
}
},
props: ['parentData'],
created () {
// 这里将输出 parent
console.log(this.childData)
}
}
var vm = new Vue({
el: '#app',
// 通过 props 向子组件传递数据
template: '<child parent-data="parent" />',
components: {
Child
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
如上例所示,子组件的数据 childData
的初始值就是 parentData
这个 props
。而之所以能够这样做的原因有两个
- 1、由于
props
的初始化先于data
选项的初始化 - 2、
data
选项是在初始化的时候才求值的,你也可以理解为在初始化的时候才使用mergeData
进行数据合并。
三、你可以这么做。
在上面的例子中,子组件的 data
选项我们是这么写的:
data () {
return {
childData: this.parentData
}
}
2
3
4
5
但你知道吗,你也可以这么写:
data (vm) {
return {
childData: vm.parentData
}
}
// 或者使用更简单的解构赋值
data ({ parentData }) {
return {
childData: parentData
}
}
2
3
4
5
6
7
8
9
10
11
我们可以通过解构赋值的方式,也就是说 data
函数的参数就是当前实例对象。那么这个参数是在哪里传递进来的呢?其实有两个地方,其中一个地方我们前面见过了,如下面这段代码:
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
2
3
4
5
6
注意这里的 childVal.call(this, this)
和 parentVal.call(this, this)
,关键在于 call(this, this)
,可以看到,第一个 this
指定了 data
函数的作用域,而第二个 this
就是传递给 data
函数的参数。
当然了仅仅在这里这么做是不够的,比如 mergedDataFn
前面的代码:
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
2
3
4
5
6
在这段代码中,直接将 parentVal
或 childVal
返回了,我们知道这里的 parentVal
和 childVal
就是 data
函数,由于被直接返回,所以并没有指定其运行的作用域,且也没有传递当前实例作为参数,所以我们必然还是在其他地方做这些事情,而这个地方就是我们说的第二个地方,它在哪里呢?当然是初始化的时候,后面我们会讲到的,如果这里大家没有理解也不用担心。
# 2.3 选项生命周期
接下来我们再看一下选项生命周期合并策略,如下:
源码目录:src/core/util/options.js
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
2
3
这段代码是通过遍历 LIFECYCLE_HOOKS
数组,给 strats
策略对象上添加用来合并各个生命周期钩子选项的策略函数,并且这些生命周期钩子选项的策略函数相同:都是 mergeHook
函数。
LIFECYCLE_HOOKS
是一个常量,定义如下:
源码目录: src/shared/constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
2
3
4
5
6
7
8
9
10
11
12
13
所以通过上面的遍历就会在 strats
策略对象上添加如上的 11 个属性,值都为mergeHook
函数。
接下来我们再来看看mergeHook
函数,如下:
源码目录:src/core/util/options.js
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mergeHook
函数根据函数上方的注释可知最后被合并成了数组。函数体中 res
的结果由三组三元运算所得:
- 如果子选项不存在直接等于父选项的值
- 如果子选项存在,再判断父选项。如果父选项存在, 就把父子选项合并成一个数组
- 如果父选项不存在,判断子选项是不是一个数组,如果不是数组将其作为数组的元素
最后 res
有值则以它为参数调用 dedupeHooks
函数,返回值作为最终结果返回,否则直接返回 res
。其中dedupeHooks
函数目的是为了剔除选项合并数组中的重复值。
接下来我们在看看 dedupeHooks
的定义,如下:
源码目录:src/core/util/options.js
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
}
2
3
4
5
6
7
8
9
dedupeHooks
函数中通过遍历钩子数组,去掉数组中的重复值。
注意: 生命周期钩子数组按顺序执行,因此先执行父选项中的钩子函数,后执行子选项中的钩子函数。
下面我们举几个例子,来加强一下对 mergeHook
的理解,如下:
// 1
new Vue({
created: function () {
console.log('created')
}
})
2
3
4
5
6
如果以这段代码为例,那么对于 strats.created
策略函数来讲(注意这里的 strats.created
就是 mergeHooks
),childVal
就是我们例子中的 created
选项,它是一个函数。parentVal
应该是 Vue.options.created
,但 Vue.options.created
是不存在的,所以最终经过 strats.created
函数的处理将返回一个数组:
// res
options.created = [
function () {
console.log('created')
}
]
2
3
4
5
6
再看下面的例子:
// 2
const Parent = Vue.extend({
created: function () {
console.log('parentVal')
}
})
const Child = new Parent({
created: function () {
console.log('childVal')
}
})
2
3
4
5
6
7
8
9
10
11
12
其中 Child
是使用 new Parent
生成的,所以对于 Child
来讲,childVal
是:
created: function () {
console.log('childVal')
}
2
3
而 parentVal
已经不是 Vue.options.created
了,而是 Parent.options.created
,那么 Parent.options.created
是什么呢?它其实是通过 Vue.extend
函数内部的 mergeOptions
处理过的,所以它应该是这样的:
Parent.options.created = [
created: function () {
console.log('parentVal')
}
]
2
3
4
5
所以这个例子最终的结果就是既有 childVal
,又有 parentVal
,那么根据 mergeHooks
函数的逻辑执行 parentVal.concat(childVal)
这句,将 parentVal
和 childVal
合并成一个数组。所以最终结果如下:
// res
[
created: function () {
console.log('parentVal')
},
created: function () {
console.log('childVal')
}
]
2
3
4
5
6
7
8
9
另外我们注意第三个三目运算符:
: Array.isArray(childVal)
? childVal
: [childVal]
2
3
它判断了 childVal
是不是数组,这说明什么?说明了生命周期钩子是可以写成数组的,虽然 Vue
的文档里没有:
new Vue({
created: [
function () {
console.log('first')
},
function () {
console.log('second')
},
function () {
console.log('third')
}
]
})
2
3
4
5
6
7
8
9
10
11
12
13
# 2.4 选项资源
接下来我们再看一下选项资源合并策略,如下:
源码目录:src/core/util/options.js
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
2
3
这段代码是通过遍历 ASSET_TYPES
数组,给 strats
策略对象上添加用来合并各个资源选项的策略函数,并且这些资源选项的策略函数相同:都是 mergeAssets
函数。
ASSET_TYPES
是一个常量,定义如下:
源码目录: src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
2
3
4
5
所以通过上面的遍历就会在 strats
策略对象上添加如上的 3 个属性 components
、directives
、filters
,值都为mergeAssets
函数。
接下来我们再来看看mergeAssets
函数,如下:
源码目录:src/core/util/options.js
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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这段代码首先以 parentVal
为原型创建对象 res
,然后判断是否有 childVal
,如果有的话使用 extend
函数将 childVal
上的属性混合到 res
对象上并返回。如果没有 childVal
则直接返回 res
。
接来我们举个例子,来加强一下对 mergeAssets
的理解,如下:
new Vue({
el: '#app',
components: {
Child
}
})
2
3
4
5
6
上面的代码中,我们创建了一个 Vue
实例,并注册了一个子组件 Child
,此时 mergeAssets
方法内的 childVal
就是例子中的 components
选项,而 parentVal
就是 Vue.options.components
。我们在前面 vue源码分析(五) 静态属性和方法 (opens new window) 章节分析过了 Vue.options
的结构,如下:
Vue.options = {
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
2
3
4
5
6
7
8
9
10
11
12
13
所以 mergeAssets
函数的参数列表中的 parentVal
和 childVal
的值分别为,如下:
// parentVal
parentVal = {
KeepAlive
Transition,
TransitionGroup
}
// childVal
childVal = {
Child
}
2
3
4
5
6
7
8
9
10
11
所以经过 Object.create(parentVal || null)
之后,在 res
的原型上添加了 KeepAlive
、Transition
、TransitionGroup
三个属性,我们可以通过 .
语法或者 [key]
来获取对应的属性值。然后再经过 extend(res, childVal)
之后,res
变量将被添加 Child
属性,最终 res
如下:
res = {
Child,
// 原型
__proto__: {
KeepAlive,
Transition,
TransitionGroup
}
}
2
3
4
5
6
7
8
9
我们再回到 mergeAssets
函数中,看看这句话的作用,如下:
源码目录:src/core/util/options.js
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
这句话的作用是在开发环境,判断childVal
是否是一个纯对象,如果不是则报一个警告,下面我们再来看看,assertObjectType
函数的定义,如下:
源码目录:src/core/util/options.js
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
)
}
}
2
3
4
5
6
7
8
9
assertObjectType
很简单,就是使用 isPlainObject
进行判断。
# 2.5 选项 watch
接下来我们再看一下选项 watch
合并策略,如下:
源码目录:src/core/util/options.js
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这段代码是在strats
策略对象上添加 watch
属性,其值为一个函数,用来合并选项watch
的策略函数。这个函数内部首先两句代码是,如下:
源码目录:src/core/util/options.js
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
2
3
在 Firefox
浏览器中 Object.prototype
拥有原生的 watch
函数,所以即便一个普通的对象你没有定义 watch
属性,但是依然可以通过原型链访问到原生的 watch
属性,这就会给 Vue
在处理选项的时候造成迷惑,因为 Vue
也提供了一个叫做 watch
的选项,即使你的组件选项中没有写 watch
选项,但是 Vue
通过原型访问到了原生的 watch
。这不是我们想要的,所以上面两句代码的目的是一个变通方案,当发现组件选项是浏览器原生的 watch
时,那说明用户并没有提供 Vue
的 watch
选项,直接重置为 undefined
。
其中在 nativeWatch
,我们看一下它的定义,如下:
源码目录:src/core/util/env.js
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
2
nativeWatch
表示在 Firefox
中原生提供了 Object.prototype.watch
函数,所以当运行在 Firefox
中时 nativeWatch
为原生提供的函数,在其他浏览器中 nativeWatch
为 undefined
。这个变量主要用于 Vue
处理 watch
选项时与其冲突。
接下来我们回到策略函数,继续往下看,如下:
源码目录:src/core/util/env.js
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
2
3
4
5
首先检测了是否有 childVal
,即组件选项是否有 watch
选项,如果没有的话,直接以 parentVal
为原型创建对象并返回(如果有 parentVal
的话)。
如果组件选项有 watch
接下来在开发环境判断childVal
是否是一个纯对象,如果不是则报一个警告。如果是,继续判断是否有 parentVal
,如果没有的话则直接返回 childVal
,即直接使用组件选项的 watch
。如果存在 parentVal
,那么代码继续执行,此时 parentVal
以及 childVal
都将存在,那么就需要做合并处理了,如下:
源码目录:src/core/util/env.js
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
2
3
4
5
6
7
8
9
10
11
12
13
这段代码首先定一个变量 ret
用来合并 parentVal
和 childVal
,接下来使用 extend
函数将 parentVal
的属性混合到 ret
中,然后在通过 for...in
循环遍历 childVal
。
在循环体中首先分别获取遍历到的 key
在 parentVal
和 childVal
对象上的值分别为 parent
、child
,此时由于遍历的是 childVal
所以 parent
的值不一定存在有可能是 undefined
、而child
的值是一定存在的,所以接下来判断 parent
如果存在并且不是数组类型,则把其包装成数组类型赋值给 parent
,接下是两个三元运算,第一个判断parent
如果存在则把 parent
、child
合并成一个数组赋值给 ret
上对应 key
属性的值,如果不存在继续第二个三元运算判断 child
是否为数组,如果是数组则把 child
直接赋值给 ret
上对应 key
属性的值,如果不是则把 child
包装成数组赋值给 ret
上对应 key
属性的值。
和前面一样接来我们举个例子,来加强一下对选项 data
合并的理解,如下:
// 创建子类
const Sub = Vue.extend({
// 检测 test 的变化
watch: {
test: function () {
console.log('extend: test change')
}
}
})
// 使用子类创建实例
const v = new Sub({
el: '#app',
data: {
test: 1
},
// 检测 test 的变化
watch: {
test: function () {
console.log('instance: test change')
}
}
})
// 修改 test 的值
v.test = 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
上面的代码中,当我们修改 v.test
的值时,两个观察 test
变化的函数都将被执行。
我们使用子类 Sub
创建了实例 v
,对于实例 v
来讲,其 childVal
就是组件选项的 watch
:
watch: {
test: function () {
console.log('instance: test change')
}
}
2
3
4
5
而其 parentVal
就是 Sub.options
,实际上就是:
watch: {
test: function () {
console.log('extend: test change')
}
}
2
3
4
5
最终这两个 watch
选项将被合并为一个数组:
watch: {
test: [
function () {
console.log('extend: test change')
},
function () {
console.log('instance: test change')
}
]
}
2
3
4
5
6
7
8
9
10
可以发现 watch.test
变成了数组,但是 watch.test
并不一定总是数组,只有父选项(parentVal
)也存在对该字段的观测时它才是数组,如下:
// 创建实例
const v = new Vue({
el: '#app',
data: {
test: 1
},
// 检测 test 的变化
watch: {
test: function () {
console.log('instance: test change')
}
}
})
// 修改 test 的值
v.test = 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们直接使用 Vue
创建实例,这个时候对于实例 v
来说,父选项是 Vue.options
,由于 Vue.options
并没有 watch
选项,所以逻辑将直接在 strats.watch
函数的这句话中返回:
if (!parentVal) return childVal
没有 parentVal
即父选项中没有 watch
选项,则直接返回 childVal
,也就是直接返回了子选项的 watch
选项,如就是例子中写的对象:
{
test: function () {
console.log('instance: test change')
}
}
2
3
4
5
所以此时 test
字段就不再是数组了,而就是一个函数。
所以大家应该知道:被合并处理后的 watch
选项下的每个键值,有可能是一个数组,也有可能是一个函数。
# 2.6 选项数据
接下来我们再看一下选项数据合并策略,包括 props
、methods
、inject
、computed
,如下:
源码目录:src/core/util/options.js
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这段代码的作用是在 strats
策略对象上添加 props
、methods
、inject
以及 computed
策略函数,这些策略函数是分别用来合并处理同名选项的,并且所使用的策略相同。
对于 props
、methods
、inject
以及 computed
这四个选项有一个共同点,就是它们的结构都是纯对象,虽然我们在书写 props
或者 inject
选项的时候可能是一个数组,但是在上一章规范化章节中我们知道,Vue
内部都将其规范化为了一个对象。
首先,会检测 childVal
是否存在,即子选项是否有相关的属性,如果有的话在非生产环境下需要使用 assertObjectType
检测其类型,保证其类型是纯对象。然后会判断 parentVal
是否存在,不存在的话直接返回子选项。
如果 parentVal
存在,则使用 extend
方法将其属性混合到新对象 ret
中,如果 childVal
也存在的话,那么同样会再使用 extend
函数将其属性混合到 ret
中,所以如果父子选项中有相同的键,那么子选项会把父选项覆盖掉。
# 2.7 选项provide
接下来我们再看一下选项 provide
合并策略,如下:
源码目录:src/core/util/options.js
strats.provide = mergeDataOrFn
provide
选项的合并策略与 data
选项的合并策略相同,都是使用 mergeDataOrFn
函数,这里就不重复讲解了。
# 3. 自定义合并策略
关于 自定义选项合并策略 (opens new window) 可以查看官网的教程。
# 4. mixins
我们再回到 mergeOptions
看看 mixins
的处理,代码如下:
源码目录:src/core/util/options.js
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
2
3
4
5
在分析之前我们先来看一个例子,如下:
const mixin = {
created () {
console.log('this is mixins created')
}
}
new Vue ({
mixins: [mixin],
created () {
console.log('this is new Vue created ')
}
})
2
3
4
5
6
7
8
9
10
11
12
实例化执行会在控制台打印出如下的内容:
'this is mixins created'
'this is new Vue created'
2
mergeOptions
函数在处理 mixins
选项的时候递归调用了 mergeOptions
函数将 mixins
合并到了 parent
中,并将合并后生成的新对象作为新的 parent
。
在我们这个例子中会循环合并 mixins
中的所有选项,当前只有一个 created
生命周期钩子函数,所以最终会调用生命周期的合并策略来处理即调用mergeHook
,我们知道生命周期合并策略最终是将相同名称的钩子被合并成了数组,所以所有的生命周期钩子都会被执行。
除了生命周期,任何写在 mixins
中的选项,都会使用 mergeOptions
中相应的合并策略进行处理,这就是 mixins
的实现方式。
# 5. extends
extends
选项,与 mixins
相同,只是由于 extends
选项只能是一个对象,而不能是数组,所以不需要遍历。
# 6. 总结
现在我们了解了 Vue
中是如何合并处理选项的,接下来我们做一个总结:
- 对于
el
、propsData
选项使用默认的合并策略defaultStrat
。 - 对于
data
选项,使用mergeDataOrFn
函数进行处理,最终结果是data
选项将变成一个函数,且该函数的执行结果为真正的数据对象。 - 对于
生命周期钩子
选项,将合并成数组,使得父子选项中的钩子函数都能够被执行 - 对于
directives
、filters
以及components
等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。 - 对于
watch
选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。 - 对于
props
、methods
、inject
、computed
选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。 - 对于
provide
选项,其合并策略使用与data
选项相同的mergeDataOrFn
函数。 - 最后,以上没有提及到的选项都将使默认选项
defaultStrat
。 - 最最后,默认合并策略函数
defaultStrat
的策略是:只要子选项不是undefined
就使用子选项,否则使用父选项。
我们这个案例经过选项合并后的 $options
为,如下
vm.$options = {
components: {},
directives: {},
filters: {},
el: "#app",
template: "<div> {{ name }} </div>",
data: function mergedInstanceDataFn() {},
_base: function Vue(options){}
}
2
3
4
5
6
7
8
9