数据响应式更新原理解析

数据响应式更新原理

现代的前端框架都引入了数据的响应式更新系统,modal层更新的数据能实时的影响到view层,这为我们的开发带来了极大的便利。这些的响应原理是什么呢。

ES6之前的响应式

JavaScript 中对象的每个属性都拥有一个属性描述对象,用来定义该属性的行为。Object.defineProperty 是 ES5.1 规范中提供的方法,用来修改对象属性的属性描述对象,文档可参见 MDN-Object.defineProperty()。通过 Object.defineProperty 函数,我们可以通过定义对象属性的存取器(getter/setter)来劫持数据的读取,实现数据变动时的通知功能。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let o = {
a: 1
}
let value = o.a
Object.defineProperty(o, 'a', {
enumerable: true,
configurable: true,
get () {
console.log('a 属性被访问')
return value
},
set (newValue) {
console.log('a 属性被修改了,新值为', newValue, '旧值为', value)
value = newValue
}
})

通过对对象属性的存储器(getter/setter)进行劫持,使我们能够在对象的属性更改后得到通知,这就是vue的响应式更新的原理。由于该方法不能够被polyfill,所以不支持ie9一下浏览器

数据响应变化

Object.defineProperty方法一次只能定义一个键值的属性描述对象,因此我们可以很容易的写出一个监测函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let o = {
a: 1
}
function watch (obj, key, callback) {
let value = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
return value
},
set (newValue) {
let oldValue = value
value = newValue
callback(newValue, oldValue)
}
})
}
//『使数据响应化』和『添加回调函数』两个操作现在是耦合在一起的
watch(o, 'a', (newValue, oldValue) => { // 使 o.a 响应化
console.log('属性被修改了,新值为:', newValue, ',旧值为:', oldValue)
})
o.a = 2 // 输出: 属性被修改了,新值为: 2 ,旧值为: 1
o.a = 3 // 输出: 属性被修改了,新值为: 3 ,旧值为: 2

递归遍历整个对象

为了让整个对象响应化,我们需要遍历对象中的所有键并为其应用 Object.defineProperty 方法。对于键值为对象的情况,递归进去处理。同时,为了让 『使数据响应化』和『添加回调函数』两个操作解耦,我们引入 DepWatcher 类,使用订阅/发布模式向响应式数据注册回调函数。

1
2
3
4
5
6
7
变动时通知
data --------------> Dep类 收集回调
| ^
发布 | |
| |订阅
^ |
Watcher
1
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
// Dep 类 保存数据源的所有订阅
// 并且在接受到数据变动后出发所有订阅
class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
},
notify (newValue, oldValue) {
this.subs.forEach(sub => {
sub.update(newValue, oldValue)
})
}
}
const globalDep = new Dep()
// Watcher 类 ,每一个Watcher为一个订阅源
class Watcher {
constructor (callback) {
this.callback = callback
}
update (newValue, oldValue) {
this.callback(newValue, oldValue)
}
}
// 使一个对象响应化
function observify(value) {
if (!isObject(value)) {
return false
}
Object.keys(value).forEach(key => {
defineReactive(value, key, value[key]) // 遍历每个键值 使其响应化
})
}
function isObject(value) {
return typeof value === 'object' && value !== null
}
function defineReactive (obj, key, value) {
observify(value) // 递归
Object.defineProperty(obj, key, {
enmuerable: true,
configurable: true,
get () {
return value
},
set (newValue) {
if (newValue === value) {
return
} else {
let oldValue = value
value = newValue
observify(newValue)
globalDep.notify(newValue, oldValue)
}
}
})
}
let o = {
a: 1,
c: {
d: 1
}
}
//『使数据响应化』和『添加回调函数』两个操作已被解耦,解耦后可以方便的多次添加订阅
// 使数据响应化
observify(o)
// 添加订阅
globalDep.addSub(new Watcher((newValue, oldValue) => {
console.log('发生改变!新值:', newValue, ",旧值:", oldValue)
}))
o.a = 2 // 输出:发生改变!新值: 2 ,旧值: 1
o.c.d = 4 // 输出:发生改变!新值: 4 ,旧值: 1
// 可以再添加一个订阅
globalDep.addSub(new Watcher((newValue, oldValue) => {
console.log('新订阅')
}))
o.a = 3 // 输出:发生改变!新值: 3 ,旧值: 2
// 输出:新订阅