Vue实现数据响应式的原理
# Vue实现数据响应式的原理
源码版本:2.6.14(增删了一些注释)
响式是指,data中数据改变时自动更新视图。
实现数据的响应式,一共分为四个组成部分:
# Observer
使数据的改变成为可观测的
在Observer类中,用递归的方法通过Object.defineProperty
为属性添加getter和setter,使数据变为响应式。
对于数组属性,则进行了额外处理。
Observer类的源码实现:
// 源码位置:src/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) // 将该数据的__ob__属性指向该Observer,表示它已是响应式的了
if (Array.isArray(value)) { // 对于数组和对象分别处理
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 为数据定义响应式的属性,即在setter中进行收集,在getter中触发更新
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
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
}
// 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]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# Dep
收集依赖
# 什么是收集依赖?
一个数据可能会在多个地方使用,因此对应一个数据使用一个Dep收集所有依赖该数据的视图。当数据发生变化时,将Dep里的所有视图进行更新即可
# 如何进行?
在Observer中对于数据定义了getter和setter,因此:
- 在getter中进行依赖收集
- 在setter中通知依赖更新
在Dep类中,用一个subs数组存放Watcher,并定义了对subs进行增删操作的函数。
# Watcher
谁用了数据,谁就是“依赖”,就为谁创建一个Watcher
进行依赖收集的过程:
- 在构造函数中调用get方法,最终通过pushTarget将自身赋值给
Dep.target
get () {
pushTarget(this) // 给Dep.target赋值
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 通过this.getter获取被依赖的数据,在数据的getter中会触发其
dep.depend
方法,收集依赖。并在收集完成后将Dep.target释放
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
},
2
3
4
5
6
7
8
9
10
11
12
13
- 数据变化时,触发数据的setter,在其中调用
dep.notify
方法,触发subs数组中的所有Watcher实例的update方法
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 对于数组的处理
我们知道,对于数组直接进行修改无法触发响应式更新,但可以通过Array的七个API进行修改。
原因是对于这七个API进行了拦截,方法是通过更改数组的原型链
- 首先,定义一个新的原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
2
- 定义需要拦截的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
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
为什么这里对push、unshift、splice做了判断?
这三种方法会添加新元素。
- 在Observer的构造函数中,更改数组的
__proto__
属性
if (Array.isArray(value)) {
if (hasProto) { // 如果可以使用__proto__属性
protoAugment(value, arrayMethods) // 更改原型链
} else {
copyAugment(value, arrayMethods, arrayKeys) // 如果不支持,重新定义这七种方法
}
this.observeArray(value)
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
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
12
13
14
15
16
17
18
# 总结
- Observer类通过
Object.defineProperty
将数据对象转换为可观测的 - 为每一个依赖创建一个Watcher,会添加到对应的Dep中。当数据变化时,Dep会通知Watcher,然后Watcher通知依赖进行更新
- 用Dep类实现了对依赖的收集,当数据改变时通知其中的所有依赖
# 为什么$set可以保持响应式
set方法的定义源码:
export function set (target: Array<any> | Object, key: any, val: any): any {
// 在非生产环境下,如果传入的target是null、undefined、原始数据类型,则给出警告
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果target是数组,用splice这个API进行更改
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 如果key是target中已存在的属性,则直接更改即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 这里的修改会触发该属性的setter,并通知更新
return val
}
const ob = (target: any).__ob__ // 如果target已进行响应式处理,则ob是其Observer实例,否则不存在
// 如果target是Vue实例或Vue实例的data对象,抛出警告
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
}
// ob不存在,说明target不是响应式的,因此直接添加属性即可
if (!ob) {
target[key] = val
return val
}
// target是响应式的,则通过defineReactive响应式的定义该属性,并通知更新
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
32
33
34
35
36
37
38
39
40
41
42
43
通过源码可得知:
- 如果目标是数组,首先判断索引是否大于当前的length,若大于则先修改length。然后调用splice方法
- 如果是对象,则判断属性是否存在、该对象是否是响应式的:
- 如果该属性已经存在,或该对象不是响应式的,则直接赋值
- 否则:调用内部
defineReactive
方法,响应式的添加属性,并通知更新