vue源码分析(十八) 数据响应系统 —— Observe
# 1. 概述
上一章我们分析初始化的时候预留了 initState
,我们知道 initState
的作用是初始化 props
属性、data
属性、methods
属性、computed
属性、watch
属性。
而在我们 Vue
响应式系统中,data
是响应式系统的核心。所以我们这一章节,先从 data
的初始化开始分析,最后回过头来分析其他属性的初始化。
在分析之前我们还是以一个案例来进行讲解,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>深入响应式原理-响应式对象</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
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
# 2. initData
上一章节我们分析 initState
的定义,下面我们来分析 data
的初始化,首先我们来看 data 初始化的入口,如下:
源码目录:scr/core/instance/state.js
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
2
3
4
5
这段代码首先判断 data
选项是否存在,如果存在则调用 initData
初始化 data
选项,如果不存在则直接调用 observe
函数观测一个空对象:{}
,即 observe
函数是将 data
转换成响应式数据的核心入口,$data
属性是一个访问器属性,其代理的值就是 _data
。
接下来我们再来看看 initData
的定义,由于这个函数的代码比较多,我们将分解来看,如下:
源码目录:scr/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 省略...
}
2
3
4
5
6
7
这段代码首先定义一个变量 data
,它的值是 vm.$options.data
的引用。我们在 vue源码分析(十六) 选项合并之合并策略 (opens new window) 分析过 vm.$options.data
被合并最终的结果是一个函数,此函数的调用的返回对象才是真正的数据。
接下来判断 data
的类型,如果 data
是函数类型,则通过 getData(data, vm)
获取真正的数据,如果 data
不是函数类型,则直接返回 data
或 空对象,最后将该对象赋值给 vm._data
属性,同时重写了 data
变量,此时 data
变量已经不是函数了,而是最终的数据对象。
getData
是如何获取真实数据的?我们先来看一下它的定义如下:
源码目录:scr/core/instance/state.js
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
2
3
4
5
6
7
8
9
10
11
12
getData
函数接收两个参数 data
为合并后的函数,vm
为当前 Vue
实例。函数内部首先调用 pushTarget
函数,最后有调用 popTarget
函数,中间是一个 try...catch...finally
语句,其中 pushTarget
和 popTarget
为了防止使用 props
数据初始化 data
数据时收集冗余的依赖,在这里我们对这两个函数不多做分析,只了解一下它们的作用,知道它们是做什么的就行,等到我们分析 Vue
是如何收集依赖的时候会回头来说明。
try...catch...finally
语句中,为了捕获 data
函数中可能出现的错误。同时如果有错误发生那么则返回一个空对象作为数据对象:return {}
。没有错误的情况下,调用 data
函数获取真正的数据对象并返回,即:data.call(vm, vm)
。
我们回到 initData
函数,继续往下看,代码如下:
源码目录:scr/core/instance/state.js
export function getData (data: Function, vm: Component): any {
// 省略...
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// 省略...
}
2
3
4
5
6
7
8
9
10
11
12
上面这段代码是判断 data
是否是一个纯对象,如果不是一个纯对象首先将 data
的值设置成空对象,并且在开发环境中报一个警告。
继续往下看,代码如下:
源码目录:scr/core/instance/state.js
export function getData (data: Function, vm: Component): any {
// 省略...
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// 省略...
}
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
这段代码,首先通过 Object.keys
获取到 data
对象中的所有属性的名称,并将由 data
对象的键所组成的数组赋值给 keys
常量。接下来分别用常量 props
和 methods
保存 vm.$options.props
和 vm.$options.methods
的引用。
接下来是一个 while
循环,用来遍历data
对象的键所组成的数组即 keys
,while
循环体中是两个 if
语句块和一个 else if
语句块。
首先我们来分析第一个 if
语句块,首先判断是否在开发环境,如果在开发环境,则继续判断data
数据的 key
与 methods
对象中定义的函数名称是否相同,如果相同 那么会打印一个警告,提示开发者:你定义在 methods
对象中的函数名称已经被作为 data
对象中某个数据字段的 key
了 。我们知道定义在 data
中的数据对象,还是定义在 methods
对象中的函数,都可以通过实例对象代理访问,即vm.xxx
。所以当 data
数据对象中的 key
与 methods
对象中的 key
冲突时,就会产生覆盖掉的现象,所以为了避免覆盖 Vue
是不允许在 methods
中定义与 data
字段的 key
重名的函数的。
我们再来看看第二个 if
语句块,首先判断data
数据的 key
与 props
对象中定义的数据对象的key
是否相同,如果相同则继续判断是否在开发环境,如果在开发环境那么会打印一个警告。同 methods
一样 props
中的数据,也可以通过实例对象代理访问,所以也会产生覆盖掉的现象。
优先级的关系:props优先级 > methods优先级 > data优先级 ,即如果一个 key
在 props
中有定义了那么就不能在 data
和 methods
中出现了;如果一个 key
在 data
中出现了那么就不能在 methods
中出现了。
最后是执行 else if
语句块,判断定义在 data
中的 key
是否是保留键,即是否是以 $
或 _
开头的,如果不是将执行 proxy
函数,实现实例对象的代理访问,proxy
的作用是将 _data
的属性值代理到 Vue
实例上的相同属性值上,例如 vm.msg = vm._data.msg
。
说明:Vue
是不会代理那些键名以 $
或 _
开头的字段的,因为 Vue
自身的属性和方法都是以 $
或 _
开头的,所以这么做是为了避免与 Vue
自身的属性和方法相冲突。
proxy
是如何实现代理的?我们先来看一下它的定义如下:
源码目录:scr/core/instance/state.js
// proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
2
3
4
5
6
7
8
9
10
proxy
函数的原理是通过 Object.defineProperty
函数在实例对象 vm
上定义与 data
数据字段同名的访问器属性,并且这些属性代理的值是 vm._data
上对应属性的值。在我们的案例中的 data
选择有个名为 name
的属性,我们可以通过 vm.name
来访问,而实际访问的是 vm._data.name
,而这里 vm._data` 才是真正的数据对象。
我们回到 initData
函数,继续往下看,代码如下:
源码目录:scr/core/instance/state.js
// observe data
observe(data, true /* asRootData */)
2
最后通过调用 observe
函数将 data
数据对象转换成响应式的。
至此 initData
函数我们已经分析完了,这里我们总结一下 initData
主要的作用:
- 根据
vm.$options.data
选项获取真正想要的数据(注意:此时vm.$options.data
是函数) - 校验得到的数据是否是一个纯对象
- 检查数据对象
data
上的键是否与props
对象上的键冲突 - 检查
methods
对象上的键是否与data
对象上的键冲突 - 在
Vue
实例对象上添加代理访问数据对象的同名属性 - 最后调用
observe
函数开启响应式之路
# 3. observe
上一小节我们分析了 initData
的整体流程,这一小节我们来分析 observe
工厂函数。
源码目录:scr/core/observer/index.js
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
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
首先我们看一下 observe
函数的参数列表,总共有两个参数,第一个参数 value
代表要观测的数据,第二个参数 asRootData
代表要被观测的数据是否是根级数据,是一个布尔值。
接下来我们在看看 observe
函数体,首先是一个 if
语句判断要观测的数据如果不是一个纯对象或者要观测的数据类型为 VNode
则直接返回。
我们继续往下看,接下来是定义一个变量 ob
用来保存 Observer
实例,接下来又是一个 if...else if...
语句块。
我们先来看看 if
语句块的判断条件,首先使用 hasOwn
函数检测数据对象 value
自身是否含有 __ob__
属性,并且 __ob__
属性应该是 Observer
的实例。如果判断条件为真则直接将数据对象自身的 __ob__
属性的值作为 ob
的值:ob = value.__ob__
。那么 __ob__
是什么呢?其实当一个数据对象被观测之后将会在该对象上定义 __ob__
属性,所以 if
分支的作用是用来避免重复观测一个数据对象。
接下来我们在来看看 else if
语句块,同样我们先看看这个分支的判断条件:
shouldObserve
必须为true
我们先来看看shouldObserve
的定义,如下:
源码目录:scr/core/observer/index.js
/**
* In some cases we may want to disable observation inside a component's
* update computation.
*/
export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
shouldObserve = value
}
2
3
4
5
6
7
8
9
shouldObserve
变量的初始值为 true
,在 shouldObserve
变量的下面定义了 toggleObserving
函数,该函数接收一个布尔值参数,用来切换 shouldObserve
变量的真假值,我们可以把 shouldObserve
想象成一个开关,为 true
时说明打开了开关,此时可以对数据进行观测,为 false
时可以理解为关闭了开关,此时数据对象将不会被观测。
!isServerRendering()
必须为真,即isServerRendering()
函数的返回值是一个布尔值,用来判断是否是服务端渲染。也就是说只有当不是服务端渲染的时候才会观测数据(Array.isArray(value) || isPlainObject(value))
必须为真,即只有当数据对象是数组或纯对象的时候,才有必要对其进行观测。Object.isExtensible(value)
必须为真,即要被观测的数据对象必须是可扩展的。一个普通的对象默认就是可扩展的,以下三个方法都可以使得一个对象变得不可扩展:Object.preventExtensions()
、Object.freeze()
以及Object.seal()
。!value._isVue
必须为真,即Vue
实例对象拥有_isVue
属性,所以这个条件用来避免Vue
实例对象被观测。
当一个对象满足了以上五个条件时,就会执行 else...if
语句块的代码,即创建一个 Observer
实例。
# 4. Observe
上面我们讲解了 observe
工厂函数的实现,它内部是通过实例化 Observe
实现的,接下来我们先看一下 Observe
的定义,如下:
源码目录:scr/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
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
42
43
上面代码是我们 Observer
构造器的全部代码,我们可以看到 Observer
类的实例对象将拥有三个实例属性,分别是 value
、dep
和 vmCount
以及两个实例方法 walk
和 observeArray
。Observer
类的构造函数接收一个参数,即数据对象。
# 4.1 constructor
首先 我们来分析一下构造方法 constructor
,代码如下:
源码目录:scr/core/observer/index.js
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor
(opens new window) 是一种用于创建和初始化class
创建的对象的特殊方法。在 Observer
的 constructor
方法的参数就是在实例化 Observer
实例时传递的参数,即数据对象本身,可以发现,实例对象的 value
属性引用了数据对象,即 this.value = value
。
接下来是实例对象的 dep
属性的值是新创建的一个 Dep
实例。继续实例对象的 vmCount
属性被设置为 0
,即 this.vmCount = 0
。
接下来是使用 def
函数,为数据对象定义了一个 __ob__
属性,这个属性的值就是当前 Observer
实例对象。其中 def
函数其实就是 Object.defineProperty
函数的简单封装,之所以这里使用 def
函数定义 __ob__
属性是因为这样可以定义不可枚举的属性,这样后面遍历数据对象的时候就能够防止遍历到 __ob__
属性。
在我们当前案例中,数据如下:
data = {
name: 'robin'
}
2
3
经过 def
函数处理之后,data
对象应该变成如下这个样子:
data = {
name: "robin",
// __ob__ 是不可枚举的属性
__ob__: {
dep: Dep {id: 2, subs: Array(0)}, // new Dep()
value: { name: "robin", __ob__: Observer }, // value 属性指向 data 数据对象本身,这是一个循环引用
vmCount: 0
}
}
2
3
4
5
6
7
8
9
我们回到 constructor
继续往下看,接下来是一个 if...else...
语句块,判断条件是数据是不是一个数组,如果是数组执行 if
语句块,如果是对象执行 else
语句块。
我们首先来看 if
语句块,代码如下:
源码目录:scr/core/observer/index.js
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 如果value是数组,对数组每一个元素执行observe方法
this.observeArray(value)
2
3
4
5
6
7
这段代码,首先判断 hasProto
的真假,如果 hasProto
为真则调用 protoAugment
,否则调用 copyAugment
。其中 hasProto
是一个布尔值,它用来检测当前环境是否可以使用 __proto__
属性,如果 hasProto
为真则当前环境支持 __proto__
属性,否则意味着当前环境不能够使用 __proto__
属性。
如果当前环境支持使用 __proto__
属性,那么调用 protoAugment
;如果不支持,那么调用 copyAugment
函数。总之无论是 protoAugment
函数还是 copyAugment
函数,他们的目的只有一个:把数组实例与代理原型或与代理原型中定义的函数联系起来,从而拦截数组变异方法。
最后通过调用 this.observeArray(value)
递归观测数组元素。
我们再来看看 else
语句块,当数据是纯对象的情况,执行 this.walk(value)
函数。
# 4.2 protoAugment
接下来我们看看 protoAugment
定义如下:
源码目录:scr/core/observer/index.js
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
2
3
4
5
6
7
8
9
10
11
这个函数的作用是将数组实例的原型指向代理原型(arrayMethods
),在构造方法中调用 protoAugment
传递的参数是 value
和 arrayMethods
,value
是数组数据,arrayMethods
是代理原型。下面我们看看 arrayMethods
的定义如下:
源码目录:src/core/observer/array.js
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
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
42
43
44
45
这段代码义开始就是缓存数组原型对象,arrayMethods
对象的原型是真正的数组构造函数的原型。接着定义了 methodsToPatch
常量,methodsToPatch
常量是一个数组,包含了所有需要拦截的数组变异方法的名字。再往下是一个 forEach
循环,用来遍历 methodsToPatch
数组。该循环的主要目的就是使用 def
函数在 arrayMethods
对象上定义与数组变异方法同名的函数,从而做到拦截的目的。
循环体中,首先缓存了数组原本的变异方法,然后使用 def
函数在 arrayMethods
上定义与数组变异方法同名的函数,在函数体内优先调用了缓存下来的数组变异方法。并将数组原本变异方法的返回值赋值给 result
常量,并且我们发现函数体的最后一行代码将 result
作为返回值返回。这就保证了拦截函数的功能与数组原本变异方法的功能是一致的。
接下来定义了 ob
常量,它是 this.__ob__
的引用,其中 this
其实就是数组实例本身,我们知道无论是数组还是对象,都将会被定义一个 __ob__
属性,并且 __ob__.dep
中收集了所有该对象(或数组)的依赖(观察者)。所以上面两句代码的目的其实很简单,当调用数组变异方法时,必然修改了数组,所以这个时候需要将该数组的所有依赖(观察者)全部拿出来执行,即:ob.dep.notify()
。
我们继续往下看,首先定义了 inserted
变量,这个变量用来保存那些被新添加进来的数组元素。接着是一个 switch
语句,在 switch
语句中,当遇到 push
和 unshift
操作时,那么新增的元素实际上就是传递给这两个方法的参数,所以可以直接将 inserted
的值设置为 args
:inserted = args
。当遇到 splice
操作时,我们知道 splice
函数从第三个参数开始到最后一个参数都是数组的新增元素,所以直接使用 args.slice(2)
作为 inserted
的值即可。最后 inserted
变量中所保存的就是新增的数组元素,我们只需要调用 observeArray
函数对其进行观测即可。
为什么要特殊处理 push
、unshift
和 splice
呢?原因很简单,因为新增加的元素是非响应式的,所以我们需要获取到这些新元素,并将其变为响应式数据才行,而这就是switch
代码的目的。
# 4.3 copyAugment
上一小节我们分析了在当前环境支持 __proto__
属性的情况,这一小节我们继续分析在不支持的情况下,调用 copyAugment
函数,copyAugment
定义如下:
源码目录:scr/core/observer/index.js
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
2
3
4
5
6
7
8
9
10
11
copyAugment
函数的参数总共有三个,前两个和 protoAugment
一样即数组数据和代理原型,第三个参数 keys
就是定义在 arrayMethods
对象上的所有函数的键,即所有要拦截的数组变异方法的名称。函数体中通过 for
循环对 keys
进行遍历,并使用 def
函数在数组实例上定义与数组变异方法同名的且不可枚举的函数,这样就实现了拦截操作。
# 4.4 observeArray
我们知道在观测数据是数组的情况下,最终都是通过 observeArray
方法递归的观测那些类型为数组或对象的数组元素。
下面我们看看 observeArray
的定义,如下:
源码目录:scr/core/observer/index.js
/**
* Observe a list of Array items.
*/
// 如果要观察的对象时数组, 遍历数组,然后调用observe方法将对象的属性转化为响应式属性
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
2
3
4
5
6
7
8
9
observeArray
方法的实现很简单,只需要对数组进行遍历,并对数组元素逐个应用 observe
工厂函数即可,这样就会递归观测数组元素了。
# 4.5 walk
前面几个小节我们分析,当要观测的数据是数组的情况,这一小节我们将继续分析当要观测的数据是纯对象的情况。通过前面的分析我们知道观测数据是纯对象的情况下,会调用 walk
函数。
下面我们看看 walk
的定义,如下:
源码目录:scr/core/observer/index.js
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
2
3
4
5
6
7
8
9
10
11
walk
方法,首先使用 Object.keys(obj)
获取对象所有可枚举的属性,然后使用 for
循环遍历这些属性,同时为每个属性调用了 defineReactive
函数。
# 4.6 defineReactive
我们继续来看看 defineReactive
函数的定义,由于 defineReactive
的代码比较多,我们也是采用分段分析的方法来讲解这个函数,首先看一下如下代码:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略 ...
}
2
3
4
5
6
7
8
9
10
11
12
我们先来看看 defineReactive
函数的参数列表,总共有五个参数:
obj
: 要观测的数据对象key
: 属性的键名即key
val
: 对象属性对应的值customSetter
: 自定setter
shallow
: 是否浅观测
defineReactive
函数的核心就是 将数据对象的数据属性转换为访问器属性,即为数据对象的属性设置一对 getter/setter
。
我们继续往下看,代码如下:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 省略 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
首先定义一个 dep
常量引用 Dep
实例对象。接着通过 Object.getOwnPropertyDescriptor
函数获取该字段可能已有的属性描述对象,并将该对象保存在 property
常量中,接着是一个 if
语句块,判断该字段是否是可配置的,如果不可配置(property.configurable === false
),那么直接 return
,即不会继续执行 defineReactive
函数。这么做也是合理的,因为一个不可配置的属性是不能使用也没必要使用 Object.defineProperty
改变其属性定义的。
我们继续往下看,代码如下:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略 ...
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 省略 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这段代码的前两句定义了 getter
和 setter
常量,分别保存了来自 property
对象的 get
和 set
函数,我们知道 property
对象是属性的描述对象,一个对象的属性很可能已经是一个访问器属性了,所以该属性很可能已经存在 get
或 set
方法。由于接下来会使用 Object.defineProperty
函数重新定义属性的 setter/getter
,这会导致属性原有的 set
和 get
方法被覆盖,所以要将属性原有的 setter/getter
缓存,并在重新定义的 set
和 get
方法中调用缓存的函数,从而做到不影响属性的原有读写操作。
接下来 if
分支语句,当发现调用 defineReactive
函数时传递了两个参数,同时只有在属性没有 get
函数或有 set
函数的情况下才会通过 val = obj[key]
取值。为什么要这么做呢?
因为当属性原本存在 get
拦截器函数时,在初始化的时候不要触发 get
函数,只有当真正的获取该属性的值的时候,再通过调用缓存下来的属性原本的 getter
函数取值即可。所以看到这里我们能够发现,如果数据对象的某个属性原本就拥有自己的 get
函数,那么这个属性就不会被深度观测,因为当属性原本存在 getter
时,是不会触发取值动作的,即 val = obj[key]
不会执行,所以 val
是 undefined
,这就导致在后面深度观测的语句中传递给 observe
函数的参数是 undefined
。
举个例子,如下:
const data = {
getterProp: {
a: 1
}
}
new Vue({
data,
watch: {
'getterProp.a': () => {
console.log('这句话会输出')
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
上面的代码中,我们定义了数据 data
,data
是一个嵌套的对象,在 watch
选项中观察了属性 getterProp.a
,当我们修改 getterProp.a
的值时,以上代码是能够正常输出的,这也是预期行为。再看如下代码:
const data = {}
Object.defineProperty(data, 'getterProp', {
enumerable: true,
configurable: true,
get: () => {
return {
a: 1
}
}
})
const ins = new Vue({
data,
watch: {
'getterProp.a': () => {
console.log('这句话不会输出')
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们仅仅修改了定义数据对象 data
的方式,此时 data.getterProp
本身已经是一个访问器属性,且拥有 get
方法。此时当我们尝试修改 getterProp.a
的值时,在 watch
中观察 getterProp.a
的函数不会被执行。这是因为属性 getterProp
是一个拥有 get
拦截器函数的访问器属性,而当 Vue
发现该属性拥有原本的 getter
时,是不会深度观测的。
那么为什么当属性拥有自己的 getter
时就不会对其深度观测了呢?有两方面的原因,第一:由于当属性存在原本的 getter
时在深度观测之前不会取值,所以在深度观测语句执行之前取不到属性值从而无法深度观测。第二:之所以在深度观测之前不取值是因为属性原本的 getter
由用户定义,用户可能在 getter
中做任何意想不到的事情,这么做是出于避免引发不可预见行为的考虑。
我们知道当数据对象的某一个属性只拥有 get
拦截器函数而没有 set
拦截器函数时,此时该属性不会被深度观测。但是经过 defineReactive
函数的处理之后,该属性将被重新定义 getter
和 setter
,此时该属性变成了既拥有 get
函数又拥有 set
函数。并且当我们尝试给该属性重新赋值时,那么新的值将会被观测。这时候矛盾就产生了:原本该属性不会被深度观测,但是重新赋值之后,新的值却被观测了。
这就是所谓的 定义响应式数据时行为的不一致,为了解决这个问题,采用的办法是当属性拥有原本的 setter
时,即使拥有 getter
也要获取属性值并观测之。
我们继续往下看,代码如下:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略 ...
let childOb = !shallow && observe(val)
// 省略 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这段代码定义了 childOb
变量,我们知道,在 if
语句块里面,获取到了对象属性的值 val
,但是 val
本身有可能也是一个对象,那么此时应该继续调用 observe(val)
函数观测该对象从而深度观测数据对象。但前提是 defineReactive
函数的最后一个参数 shallow
应该是假,即 !shallow
为真时才会继续调用 observe
函数深度观测,由于在 walk
函数中调用 defineReactive
函数时没有传递 shallow
参数,所以该参数是 undefined
,那么也就是说默认就是深度观测。其实非深度观测的场景我们早就遇到过了,即 initRender
函数中在 Vue
实例对象上定义 $attrs
属性和 $listeners
属性时就是非深度观测,,如下:
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) // 最后一个参数 shallow 为 true
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
2
大家要注意一个问题,即使用 observe(val)
深度观测数据对象时,这里的 val
未必有值,因为必须在满足条件 (!getter || setter) && arguments.length === 2
时,才会触发取值的动作:val = obj[key]
,所以一旦不满足条件即使属性是有值的但是由于没有触发取值的动作,所以 val
依然是 undefined
。这就会导致深度观测无效。
我们继续往下看,代码如下:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略 ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 省略 ...
},
set: function reactiveSetter (newVal) {
// 省略 ...
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当执行完以上代码实际上 defineReactive
函数就执行完毕了,对于访问器属性的 get
和 set
函数是不会执行的,因为此时没有触发属性的读取和设置操作。
但是我们还是来分析一下在 get
和 set
函数中都做了哪些事情。首先我们先来看看 get
的实现,源码如下:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略 ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 省略 ...
}
})
}
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
首先判断是否存在 getter
,我们知道 getter
常量中保存的是属性原有的 get
函数,如果 getter
存在那么直接调用该函数,并以该函数的返回值作为属性的值,保证属性的原有读取操作正常运作。如果 getter
不存在则使用 val
作为属性的值。可以发现 get
函数的最后一句将 value
常量返回,这样 get
函数需要做的第一件事就完成了,即正确地返回属性值。
除了正确地返回属性值,还要收集依赖,而处于 get
函数第一行和最后一行代码中间的所有代码都是用来完成收集依赖这件事儿的,下面我们就看一下它是如何收集依赖的。
首先判断 Dep.target
是否存在,那么 Dep.target
是什么呢? Dep.target
中保存的值就是要被收集的依赖(观察者)。所以如果 Dep.target
存在的话说明有依赖需要被收集,这个时候才需要执行 if
语句块内的代码,如果 Dep.target
不存在就意味着没有需要被收集的依赖,所以当然就不需要执行 if
语句块内的代码了。
在 if
语句块内第一句执行的代码就是:dep.depend()
,执行 dep
对象的 depend
方法将依赖收集到 dep
这个“筐”中,这里的 dep
对象就是属性的 getter/setter
通过闭包引用的“筐”。
接着又判断了 childOb
是否存在,如果存在那么就执行 childOb.dep.depend()
,这段代码是什么意思呢?要想搞清楚这段代码的作用,你需要知道 childOb
是什么,前面我们分析过,假设有如下数据对象:
const data = {
a: {
b: 1
}
}
2
3
4
5
该数据对象经过观测处理之后,将被添加 __ob__
属性,如下:
const data = {
a: {
b: 1,
__ob__: {value, dep, vmCount}
},
__ob__: {value, dep, vmCount}
}
2
3
4
5
6
7
对于属性 a
来讲,访问器属性 a
的 setter/getter
通过闭包引用了一个 Dep
实例对象,即属性 a
用来收集依赖的“筐”。除此之外访问器属性 a
的 setter/getter
还通过闭包引用着 childOb
,且 childOb === data.a.__ob__
所以 childOb.dep === data.a.__ob__.dep
。也就是说 childOb.dep.depend()
这句话的执行说明除了要将依赖收集到属性 a
自己的“筐”里之外,还要将同样的依赖收集到 data.a.__ob__.dep
这里”筐“里,为什么要将同样的依赖分别收集到这两个不同的”筐“里呢?其实答案就在于这两个”筐“里收集的依赖的触发时机是不同的,即作用不同,两个”筐“如下:
- 第一个”筐“是
dep
- 第二个”筐“是
childOb.dep
第一个”筐“里收集的依赖的触发时机是当属性值被修改时触发,即在 set
函数中触发:dep.notify()
。而第二个”筐“里收集的依赖的触发时机是在使用 $set
或 Vue.set
给数据对象添加新属性时触发,我们知道由于 js
语言的限制,在没有 Proxy
之前 Vue
没办法拦截到给对象添加属性的操作。所以 Vue
才提供了 $set
和 Vue.set
等方法让我们有能力给对象添加新属性的同时触发依赖,那么触发依赖是怎么做到的呢?就是通过数据对象的 __ob__
属性做到的。因为 __ob__.dep
这个”筐“里收集了与 dep
这个”筐“同样的依赖。
假设 Vue.set
函数代码如下:
Vue.set = function (obj, key, val) {
defineReactive(obj, key, val)
obj.__ob__.dep.notify()
}
2
3
4
如上代码所示,当我们使用上面的代码给 data.a
对象添加新的属性:
Vue.set(data.a, 'c', 1)
上面的代码之所以能够触发依赖,就是因为 Vue.set
函数中触发了收集在 data.a.__ob__.dep
这个”筐“中的依赖:
Vue.set = function (obj, key, val) {
defineReactive(obj, key, val)
obj.__ob__.dep.notify() // 相当于 data.a.__ob__.dep.notify()
}
Vue.set(data.a, 'c', 1)
2
3
4
5
6
所以 __ob__
属性以及 __ob__.dep
的主要作用是为了添加、删除属性时有能力触发依赖,而这就是 Vue.set
或 Vue.delete
的原理。
在 childOb.dep.depend()
这句话的下面还有一个 if
条件语句,如果读取的属性值是数组,那么需要调用 dependArray
函数逐个触发数组每个元素的依赖收集,为什么这么做呢?那是因为 Observer
类在定义响应式属性时对于纯对象和数组的处理方式是不同。
为了弄清楚这个问题,假设我们有如下代码:
<div id="demo">
{{arr}}
</div>
const vm = new Vue({
el: '#demo',
data: {
arr: [
{ a: 1 }
]
}
})
2
3
4
5
6
7
8
9
10
11
12
首先我们观察一下数据对象:
{
arr: [
{ a: 1 }
]
}
2
3
4
5
数据对象中的 arr
属性是一个数组,并且数组的一个元素是另外一个对象。上面的对象在经过观测后将变成如下这个样子:
{
arr: [
{ a: 1, __ob__ /* 我们将该 __ob__ 称为 ob2 */ },
__ob__ /* 我们将该 __ob__ 称为 ob1 */
]
}
2
3
4
5
6
如上代码的注释所示,为了便于区别和讲解,我们分别称这两个 __ob__
属性为 ob1
和 ob2
,然后我们再来观察一下模板:
<div id="demo">
{{arr}}
</div>
2
3
在模板里使用了数据 arr
,这将会触发数据对象的 arr
属性的 get
函数,我们知道 arr
属性的 get
函数通过闭包引用了两个用来收集依赖的”筐“,一个是属于 arr
属性自身的 dep
对象,另一个是 childOb.dep
对象,其中 childOb
就是 ob1
。这时依赖会被收集到这两个”筐“中,但大家要注意的是 ob2.dep
这个”筐“中,是没有收集到依赖的。有的同学会说:”模板中依赖的数据是 arr
,并不是 arr
数组的第一个对象元素,所以 ob2
没有收集到依赖很正常啊“,这是一个错误的想法,因为依赖了数组 arr
就等价于依赖了数组内的所有元素,数组内所有元素的改变都可以看做是数组的改变。但由于 ob2
没有收集到依赖,所以现在就导致如下代码触发不了响应:
vm.$set(ins.$data.arr[0], 'b', 2)
我们使用 $set
函数为 arr
数组的第一对象元素添加了一个属性 b
,这是触发不了响应的。为了能够使得这段代码可以触发响应,就必须让 ob2
收集到依赖,而这就是 dependArray
函数的作用。如下是 dependArray
函数的代码:
源码目录:scr/core/observer/index.js
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
2
3
4
5
6
7
8
9
当被读取的数据对象的属性值是数组时,会调用 dependArray
函数,该函数将通过 for
循环遍历数组,并取得数组每一个元素的值,如果该元素的值拥有 __ob__
对象和 __ob__.dep
对象,那说明该元素也是一个对象或数组,此时只需要手动执行 __ob__.dep.depend()
即可达到收集依赖的目的。同时如果发现数组的元素仍然是一个数组,那么需要递归调用 dependArray
继续收集依赖。
那么为什么数组需要这样处理,而纯对象不需要呢?那是因为 数组的索引是非响应式的。现在我们已经知道了数据响应系统对纯对象和数组的处理方式是不同,对于纯对象只需要逐个将对象的属性重新定义为访问器属性,并且当属性的值同样为纯对象时进行递归定义即可,而对于数组的处理则是通过拦截数组变异方法的方式,也就是说如下代码是触发不了响应的:
const vm = new Vue({
data: {
arr: [1, 2]
}
})
ins.arr[0] = 3 // 不能触发响应
2
3
4
5
6
7
上面的代码中我们试图修改 arr
数组的第一个元素,但这么做是触发不了响应的,因为对于数组来讲,其索引并不是“访问器属性”。正是因为数组的索引不是”访问器属性“,所以当有观察者依赖数组的某一个元素时是触发不了这个元素的 get
函数的,当然也就收集不到依赖。这个时候就是 dependArray
函数发挥作用的时候了。
我们回到 defineReactive
再来看看 set
的实现,如下:
源码目录:scr/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 省略 ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 省略 ...
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
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
我们知道 get
函数主要完成了两部分重要的工作,一个是返回正确的属性值,另一个是收集依赖。与 get
函数类似, set
函数也要完成两个重要的事情,第一正确地为属性设置新值,第二是能够触发相应的依赖。
首先 set
函数接收一个参数 newVal
,即该属性被设置的新值。接下来取得属性原有的值,然后做新旧值的对比,即新旧值全等或者新值与新值自身都不全等,同时旧值与旧值自身也不全等情况下直接 return
。
在 js
中出现一个值与自身都不全等的情况是 ,如下:
NaN === NaN // false
所以在这个条件语句中,首先 value !== value
成立那说明该属性的原有值就是 NaN
,同时 newVal !== newVal
说明为该属性设置的新值也是 NaN
,所以这个时候新旧值都是 NaN
,等价于属性的值没有变化,所以自然不需要做额外的处理了,set
函数直接 return
。
我们继续往下看,接下来又是一个 if
语句块,在开发环境中如果 customSetter
函数存在,则执行 customSetter
函数。其中 customSetter
函数是 defineReactive
函数的第四个参数。
关于 customSetter
其实我们在讲解 initRender
函数的时候就讲解过 customSetter
的作用,如下是 initRender
函数中的一段代码:
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
2
3
上面的代码中使用 defineReactive
在 Vue
实例对象 vm
上定义了 $attrs
属性,可以看到传递给 defineReactive
函数的第四个参数是一个箭头函数,这个函数就是 customSetter
,这个箭头函数的作用是当你尝试修改 vm.$attrs
属性的值时,打印一段信息:$attrs
属性是只读的。这就是 customSetter
函数的作用,用来打印辅助信息,当然除此之外你可以将 customSetter
用在任何适合使用它的地方。
我们回到 defineReactive
继续往下分析,继续判断在有 getter
函数或没有 setter
函数的情况下直接退出。
继续判断 setter
是否存在,我们知道 setter
常量存储的是属性原有的 set
函数。即如果属性原来拥有自身的 set
函数,那么应该继续使用该函数来设置属性的值,从而保证属性原有的设置操作不受影响。如果属性原本就没有 set
函数,那么就设置 val
的值:val = newVal
。
最后两句的作用是,由于属性被设置了新的值,那么假如我们为属性设置的新值是一个数组或者纯对象,那么该数组或纯对象是未被观测的,所以需要对新值进行观测,这就是第一句代码的作用,同时使用新的观测对象重写 childOb
的值。当然了,这些操作都是在 !shallow
为真的情况下,即需要深度观测的时候才会执行。最后是时候触发依赖了,我们知道 dep
是属性用来收集依赖的”筐“,现在我们需要把”筐“里的依赖都执行一下,而这就是 dep.notify()
的作用。
# 5. set & delete
正如官方文档中介绍的那样,Vue
是没有能力拦截到为一个对象(或数组)添加属性(或元素)的,而 Vue.set
和 Vue.delete
就是为了解决这个问题而诞生的。同时为了方便使用, Vue
还在实例对象上定义了 $set
和 $delete
方法,实际上 $set
和 $delete
方法仅仅是 Vue.set
和 Vue.delete
的别名。
下面我们看看 Vue.set
和 Vue.delete
定义,如下:
源码目录: src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
// 省略 ...
Vue.set = set
Vue.delete = del
// 省略 ...
}
2
3
4
5
6
7
8
可以发现 Vue.set
函数和 Vue.delete
函数的值是来自 src/core/observer/index.js
文件中定义的 set
函数和 del
函数。
接着我们再来看看 $set
和 $delete
函数的定义,如下:
源码目录: src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
// 省略 ...
// 添加一个数组数据或者对象数据
Vue.prototype.$set = set
// 删除一个数组数据或者对象数据
Vue.prototype.$delete = del
// 省略 ...
}
2
3
4
5
6
7
8
9
10
可以看到 $set
和 $delete
的值也是来自 src/core/observer/index.js
文件中定义的 set
函数和 del
函数。所以 Vue.set
其实就是 $set
,而 Vue.delete
就是 $delete
。
说明:关于Vue.set
(opens new window) 、 $set
(opens new window)、 Vue.delete
(opens new window) 、 $delete
(opens new window) 可以查看官网学习。
# 5.1 . Vue.set($set)
下面我们看看 set
函数的定义,如下:
源码目录:scr/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
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
set
函数接收三个参数,分别为:
target
:将要被添加属性的对象key
:要添加属性的键名val
:要添加属性的值
set
函数的首先是判断在开发环境,如果 set
函数的第一个参数是 undefined
或 null
或者是原始类型值,则警告信息。
接着对 target
和 key
这两个参数做了校验,如果 target
是一个数组,并且 key
是一个有效的数组索引,那么就会执行 if
语句块的内容。也就是说当我们尝试使用 Vue.set/$set
为数组设置某个元素值的时候就会执行 if
语句块的内容。
if
语句块中将数组的长度修改为 target.length
和 key
中的较大者,否则如果当要设置的元素的索引大于数组长度时 splice
无效。 我们知道 splice
变异方法能够完成数组元素的删除、添加、替换等操作。接下来而 target.splice(key, 1, val)
就利用了替换元素的能力,将指定位置元素的值替换为新值,同时由于 splice
方法本身是能够触发响应的。
再往下依然是一个 if
语句块,key
在 target
对象上,或在 target
的原型链上,同时必须不能在 Object.prototype
上,那么将要被添加属性的对象必然就是纯对象了,当给一个纯对象设置属性的时候,假设该属性已经在对象上有定义了,那么只需要直接设置该属性的值即可,这将自动触发响应,因为已存在的属性是响应式的。
接下来定义了 ob
常量,它是数据对象 __ob__
属性的引用。
在往下又是一个 if
语句块, 这个 if
语句块有两个条件,只要有一个条件成立,就会执行 if
语句块内的代码。我们来看第一个条件 target._isVue
,我们知道 Vue
实例对象拥有 _isVue
属性,所以当第一个条件成立时,那么说明你正在使用 Vue.set/$set
函数为 Vue
实例对象添加属性,为了避免属性覆盖的情况出现,Vue.set/$set
函数不允许这么做,在非生产环境下会打印警告信息。第二个条件是:(ob && ob.vmCount)
,我们知道 ob
就是 target.__ob__
那么 ob.vmCount
是什么呢?为了搞清这个问题,我们回到 observe
工厂函数,如下代码:
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 省略...
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
2
3
4
5
6
7
observe
函数接收两个参数,第二个参数指示着被观测的数据对象是否是根数据对象,什么叫根数据对象呢?那就看 asRootData
什么时候为 true
即可,我们找到 initData
函数中,如下:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 省略...
// observe data
observe(data, true /* asRootData */)}
2
3
4
5
6
7
8
9
10
可以看到在调用 observe
观测 data
对象的时候 asRootData
参数为 true
。而在后续的递归观测中调用 observe
的时候省略了 asRootData
参数。所以所谓的根数据对象就是 data
对象。这时候我们再来看如下代码:
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 省略...
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
2
3
4
5
6
7
可以发现,根数据对象将拥有一个特质,即 target.__ob__.vmCount > 0
,这样条件 (ob && ob.vmCount)
是成立的,也就是说:当使用 Vue.set/$set
函数为根数据对象添加属性时,是不被允许的。
那么为什么不允许在根数据对象上添加属性呢?因为这样做是永远触发不了依赖的。原因就是根数据对象的 Observer
实例收集不到依赖(观察者),如下:
const data = {
obj: {
a: 1
__ob__ // ob2
},
__ob__ // ob1
}
new Vue({
data
})
2
3
4
5
6
7
8
9
10
如上代码所示,ob1
就是属于根数据的 Observer
实例对象,如果想要在根数据上使用 Vue.set/$set
并触发响应:
Vue.set(data, 'someProperty', 'someVal')
那么 data
字段必须是响应式数据才行,这样当 data
字段被依赖时,才能够收集依赖(观察者)到两个“筐”中(data属性自身的 dep
以及data.__ob__
)。这样在 Vue.set/$set
函数中才有机会触发根数据的响应。但 data
本身并不是响应的,这就是问题所在。
在往看是一个 if
语句块,我们知道 target
也许原本就是非响应的,这个时候 target.__ob__
是不存在的,所以当发现 target.__ob__
不存在时,就简单的赋值即可。
最后使用 defineReactive
函数设置属性值,这是为了保证新添加的属性是响应式的。调用了 __ob__.dep.notify()
从而触发响应。这就是添加全新属性触发响应的原理。
# 5.2 . Vue.delete($delete)
下面我们看看 delete
函数的定义,如下:
源码目录:scr/core/observer/index.js
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
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
首先delete
函数检测 target
是否是 undefined
或 null
或者是原始类型值,如果是的话那么在非生产环境下会打印警告信息。
接着对 target
和 key
这两个参数做了校验,如果 target
是一个数组,并且 key
是一个有效的数组索引,那么就会执行 if
语句块的内容。也就是使用 Vue.delete/$delete
去删除一个数组的索引。与为数组添加元素类似,移除数组元素同样使用了数组的 splice
方法,大家知道这样是能够触发响应的。
与不能使用 Vue.set/$set
函数为根数据或 Vue
实例对象添加属性一样,同样不能使用 Vue.delete/$delete
删除 Vue
实例对象或根数据的属性。不允许删除 Vue
实例对象的属性,是出于安全因素的考虑。而不允许删除根数据对象的属性,是因为这样做也是触发不了响应的,关于触发不了响应的原因,我们在讲解 Vue.set/$set
时已经分析过了。
最后使用 hasOwn
函数检测 key
是否是 target
对象自身拥有的属性,如果不是那么直接返回(return
)。很好理解,如果你将要删除的属性原本就不在该对象上,那么自然什么都不需要做。
如果 key
存在于 target
对象上,那么代码将继续运行,此时将使用 delete
语句从 target
上删除属性 key
。最后判断 ob
对象是否存在,如果不存在说明 target
对象原本就不是响应的,所以直接返回(return
)即可。如果 ob
对象存在,说明 target
对象是响应的,需要触发响应才行,即执行 ob.dep.notify()
。