Vue的响应式数据是如何做到的?
变化侦测
变化侦测 = 数据观测+依赖收集+依赖更新
- 使用Object.defineProperty来使得数据变得可“观测”
- 依赖收集(Observer):是指收集视图里的部分与数据绑定的关系
- 在getter中收集依赖,在setter中通知更新依赖
- 典型的发布-订阅模式,为了解耦,新增了一个管理对象
- dep(收集某个数据相关的所有依赖),watcher(被dep通知,更新依赖)
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// observer.js
// 收集依赖
const Dep = require('./dep');
export class Observer {
constructor(value) {
this.value = value;
def(value, "__ob__", this);
if (Array.isArray(value)) {
console.log("array");
} else {
this.walk(value);
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj,keys[i]);
}
}
}
function defineReactive(obj, key, val) {
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === 'object') {
new Observer(val);
}
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend();
return val;
},
set(newval) {
if (val === newval) return;
val = newval;
dep.notify();
}
})
}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// dep.js
// 依赖管理器: 1数据 :n依赖 的一对多关系进行依赖管理,收集某个数据相关的所有依赖
export default class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
window.target && this.addSub(window.target);
}
notify() {
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
export function remove(arr, item) {
if (arr.length > 1) {
const itemIndex = arr.indexOf(item);
if (itemIndex > 1) {
return arr.splice(itemIndex, 1);
}
}
}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// watcher.js
// watcher表示依赖关系,通知视图更新
// window.target是为了拷贝一份 watcher,添加到Dep的依赖数组中
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();
}
get() {
window.target = this;
const vm = this.vm;
let value = this.getter.call(vm, vm);
window.target = undefined;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
/**
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/;
export function parsePath(path) {
if (bailRE.test(path)) return;
const segements = path.split('.');
return function (obj) {
for (let i = 0; i < segements.length; i++) {
if (!obj) return;
obj = obj[segements[i]];
}
return obj;
}
}侦测流程
vue
这套变化侦测的缺点很明显,因为利用defineProperty
来进行收集,只限于读和写已有值,当我们对obj
进行新增或者删除属性值时,它是监听不到的。所以在官网文档上的叙述上说明过,对数组或对象的直接增加或者删除会产生不期望的结果, 为了解决这一问题,特地增加了Vue.set
和Vue.delete
两个全局API 。
数组怎么办?
看到这里,对原型熟悉的人可能会问了,这种方法只针对于Obj
类型,那剩下的常用的Arr
类型或者其他类型呢?defineProperty
数组是不可能使用的,那么我们应该怎么对数组进行依赖收集和通知更新?
还是延续上面的思想:拦截,vue
将所有数组的异变方法(能改变原有数组)拦截一波,就能知道arr
啥时候被setter
了。
经常面试被问到原型,原型链的what,why,那么how???? 我觉得这就是个很巧妙的实践~
拦截数组原型上的异变方法(会改变原有宿主的方法)的代码:
1 | //代码位置 vue/src/core/observer/array.js |
数组依赖收集
无论怎样,先得用walk
让元素注入observer依赖,使得在getter
中实例化Dep
收集依赖并将数组方法拦截掉
1 | // 源码位置:/src/core/observer/index.js |
通知更新
主要是还要对数组进行深度监测和新增元素侦测,在拦截的原型上进行依赖更新。
__ob__
是在进行初始化observer的时候,在被监听者上面挂载了自己的实例,以便访问后进行依赖更新。
1 | // 源码位置:vue/src/core/observer/array.js |
总结:
vue
的变化侦测与React
对比Vdom和Angular
的脏值检测都不一样。核心是利用defineProperty
的能力,拦截所有绑定的响应式数据(data中),在拦截中添加依赖管理器Dep
来收集管理依赖,用Watcher
表示依赖关系本身,进行通知依赖更新。其中,对于数组的侦测的思路是,覆盖所有数组原型的的异变方法,在覆盖后植入依赖逻辑。这套缺点就是对数组进行下标赋值操作时,vue是侦测不到的,官网文档上多处对此有说明。
相信下次,面试官问:为什么在
vue
中对数组下标进行赋值操作会导致不正确的响应式数据结果。这种类似的问题,你一定胸有成竹。