Vue响应式原理
# 前言
本文开始进入 Vue 的核心部分:响应式处理
在看源码之前,我们先试着实现一个响应式系统
# 响应式实现
响应式的关键在于数据如何与视图进行绑定
假设我们有一个对象 data:{name:""}
,以及一个 input 节点,我们如何做到两者的绑定?
我们知道,监测对象数据变化可以用 Object.defineProperty
,监测 input 节点数据变化可以通过监听事件。因此我们可以很快的写出这样的代码
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<input id="input" />
</body>
<script>
// 初始化
var data = {
name: "test"
}
var input = document.getElementById("input")
input.value = data.name
// 双向绑定
input.addEventListener("input", function (e) {
// input 数据变化 data 跟着变化
data.name = e.target.value
})
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
val = newVal
// data 变化 input 跟着变化
input.value = val
}
})
}
defineReactive(data, "name", data.name)
</script>
</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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
控制台上设置 name, input 会进行更新, 反过来一样。
实现的关键在于闭包, get/set 实际是对闭包变量 val 进行操作。
但是发现没有,data 数据变更和 dom 更新的处理耦合在一起了,如上input.value = val
只有 data.name
才会这样处理,其他属性变更不需要这个操作,因此 set 方法需要做的通用点。
引入发布订阅设计模式的一个经典例子 EventEmitter
class EventEmitter {
constructor() {
this.events = {}
}
on(key, callback) {
this.events[key] = this.events[key] || []
this.events[key].push(callback)
}
emit(key, val) {
let events = this.events[key]
if (!events) {
return
}
for (let i = 0, m = events.length; i < m; i++) {
events[i].call(null, val)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
其他代码修改如下
// 初始化
let data = {
name: "test"
}
let input = document.getElementById("input")
let eventEmitter = new EventEmitter()
// 双向绑定
input.addEventListener("input", function (e) {
// input 数据变化 data 跟着变化
data.name = e.target.value
})
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
return val
},
set (newVal) {
val = newVal
// 通知变更
eventEmitter.emit(key, newVal)
}
})
}
defineReactive(data, "name", data.name)
eventEmitter.on("name", function (val) {
console.log("data.name 值更新为", val)
input.value = val
})
eventEmitter.emit("name", data.name)
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
新增一个绑定 name 的 dom 元素,进行 eventEmitter.on("name",callback)
即可
看上去好像可以用了,那这么写有什么弊端呢?
这里用了属性作为key,如果属性值为对象,该对象的属性被监听需要带上之前的属性值,举例
let data = {
user:{
name:""
},
["user.name"]: ""
}
eventEmitter.on("user.name") // 这里监听的是哪个属性??
2
3
4
5
6
7
可能这可以通过寻找一个不常用的字符或者提示用户 data 不支持这么设置来解决。
另外,这里每个监听的回调应该由外部定义,方便后续取消监听。
我们改用观察者模式【详见后记,应该是发布-订阅模式】,每个属性是一个观察目标,一个目标可以有多个与之相依赖的观察者,这里观察者就是绑定属性更新视图或者 vue 的 compute 属性等的封装对象。当目标状态发生改变,通知所有的观察者进行更新。
维护观察目标依赖关系的实体类 Dep
// 依赖收集
class Dep {
constructor() {
// 观察者集合
this.subs = []
}
// 添加观察者
addSub (sub) {
this.subs.push(sub)
}
// 移除观察者
removeSub (sub) {
this.subs.splice(this.subs.findIndex(watcher => watcher === sub), 1)
}
// 处理依赖
depend () {
// 调用观察者添加该依赖
const watcher = Dep.target
watcher.addDep(this)
// 为该依赖添加观察者
this.addSub(watcher)
}
// 通知观察者更新
notify () {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 临时存放观察者
Dep.target = null
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
观察者实体类 Watcher
// 观察者
class Watcher {
/**
* @param {Object} vm 上下文
* @param {Function} getter 被监听属性的getter函数,通过 Object.getOwnPropertyDescriptor 获取
* @param {Function} callback 触发更新时执行的操作
*/
constructor(vm, getter, callback) {
this.vm = vm
this.deps = []
this.getter = getter
this.callback = callback
// 手动触发 getter 绑定依赖
this.get()
}
// 添加依赖
addDep (dep) {
this.deps.push(dep)
}
// 获取被监听属性的值
get () {
Dep.target = this
// 触发被监听属性的 getter, 此时 Dep.target 非空,进行依赖处理
let value = this.getter.call(this.vm)
Dep.target = null
return value
}
// 更新视图
update () {
let value = this.getter.call(this.vm)
this.callback.call(this.vm, value)
}
}
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
其他处理修改如下:
// 初始化
let vm = {
data: {
name: "test"
}
}
let input = document.getElementById("input")
input.addEventListener("input", function (e) {
// input 数据变化 data 跟着变化
vm.data.name = e.target.value
})
// 观察目标监听
function defineReactive (obj, key, val) {
// 创建一个闭包变量,处理观察目标 key 的依赖关系
const dep = new Dep()
Object.defineProperty(obj, key, {
get () {
if (Dep.target) {
dep.depend()
}
return val
},
set (newVal) {
if (val === newVal) {
return
}
val = newVal
// 通知观察者更新
dep.notify()
}
})
}
// 简单起见,这里我们只遍历一层
function observer (data) {
if (!data || (typeof data !== 'object')) { return }
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
}
observer(vm.data)
// 创建观察者
new Watcher(vm, Object.getOwnPropertyDescriptor(vm.data, "name").get, function (val) {
console.log("data.name 更新:", val)
input.value = val
})
// 初始化 input 的值
input.value = vm.data.name
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
完整版代码如下:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<input id="input" />
<input id="input1" />
</body>
<script>
// 依赖收集
class Dep {
constructor() {
// 观察者集合
this.subs = []
}
// 添加观察者
addSub(sub) {
this.subs.push(sub)
}
// 移除观察者
removeSub(sub) {
this.subs.splice(this.subs.findIndex(watcher => watcher === sub), 1)
}
// 处理依赖
depend() {
// 调用观察者添加该依赖
const watcher = Dep.target
watcher.addDep(this)
// 为该依赖添加观察者
this.addSub(watcher)
}
// 通知观察者更新
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 临时存放观察者
Dep.target = null
// 观察者
class Watcher {
/**
* @param {Object} vm 上下文
* @param {Function} getter 被监听属性的getter函数,通过 Object.getOwnPropertyDescriptor 获取
* @param {Function} callback 触发更新时执行的操作
*/
constructor(vm, getter, callback) {
this.vm = vm
this.deps = []
this.getter = getter
this.callback = callback
// 手动触发 getter 绑定依赖
this.get()
}
// 添加依赖
addDep(dep) {
this.deps.push(dep)
}
// 获取被监听属性的值
get() {
Dep.target = this
// 触发被监听属性的 getter, 此时 Dep.target 非空,进行依赖处理
let value = this.getter.call(this.vm)
Dep.target = null
return value
}
// 更新视图
update() {
let value = this.getter.call(this.vm)
this.callback.call(this.vm, value)
}
}
// 初始化
let vm = {
data: {
name: "test"
}
}
let input = document.getElementById("input")
input.addEventListener("input", function (e) {
// input 数据变化 data 跟着变化
vm.data.name = e.target.value
})
// 观察目标监听
function defineReactive(obj, key, val) {
// 创建一个闭包变量,处理观察目标 key 的依赖关系
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (val === newVal) {
return
}
val = newVal
// 通知观察者更新
dep.notify()
}
})
}
// 简单起见,这里我们只遍历一层
function observer(data) {
if (!data || (typeof data !== 'object')) { return }
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
}
observer(vm.data)
// 创建观察者
new Watcher(vm, Object.getOwnPropertyDescriptor(vm.data, "name").get, function (val) {
console.log("data.name 更新:", val)
input.value = val
})
// 初始化 input 的值
input.value = vm.data.name
</script>
</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
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
- 监听
data.name
的 get/set , 并在其中创建依赖收集 dep - 创建观察者 watcher , 设置
Dep.target = watcher
, 触发data.name
的 get, 此时 Dep.target 有值, watcher 绑定依赖 dep - 当
data.name
的 set 方法被调用时,dep 通知 watcher 进行更新,而 watcher 会去更新视图,如 input 的值
整个过程可以用一张图来描述
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
这个就是 vue 响应式实现的简化版,下一篇我们从 data 源码入手,分析下 vue 的源码都做了哪些优化
# 后记
发布-订阅模式和观察者模式的最主要区别在于,当新增订阅者(观察者)的时候,发布者(观察目标)是否需要进行处理,前者是不进行处理的。
而我们根据上文的例子,响应式属性作为发布者(观察目标),当新增订阅者(观察者)Watcher 的时候,响应式属性并不需要进行变更,这一切都是通过 Dep 这个第三方进行的。
因此这整套设计是发布-订阅模式