Vue.js响应式原理

关于 Vue.js

Vue.js 是一款 MVVM 框架,上手快速简单易用,通过响应式在修改数据的时候更新视图。当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。Vue 通过设定对象属性的 setter/getter 方法来监听数据的变化,通过 getter 进行依赖收集,而每个 setter 方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

使 Object 数据变成可观察(observable)的

首先定义一个对象 person:

1
2
3
4
var person = {
name: "张三",
age: 18,
};

然后我们通过 Object.defineProperty 来改写这个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {};
var name = "张三";
Object.defineProperty(person, "name", {
enumerable: true,
// 可枚举
configurable: true,
// 可改变
get() {
console.log("name属性被读取了");
return name;
},
set(newVal) {
console.log("name属性被修改了");
name = newVal;
},
});
console.log(person.name); // name属性被读取了 张三
person.name = "李四"; // name属性被修改了

通过 Object.defineProperty()方法给 person 定义了一个 name 属性,并把这个属性的读和写分别使用 get()和 set()进行拦截,每当该属性进行读或写操作的时候就会触发 get()和 set()。

那么 Vue 是如何将所有 data 下面的所有属性变成可观察的(observable)呢?

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
// 源码位置(vue2.6.11):src/core/observer/index.js
/**
* def方法用来给对象obj新增一个属性key
*/
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}
/**
* Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
class Observer {
constructor(value) {
this.value = value;
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value);
}
}

walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}

/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive(obj, key, val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === "object") {
new Observer(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
console.log(`${key}属性被修改了`);
val = newVal;
},
});
}

let person = new Observer({
name: "张三",
age: 18,
});

person.value.name; // name属性被读取了 张三
person.value.name = "李四"; // name属性被修改了

在上面的代码中,我们定义了 Observer 类,它用来将一个正常的 object 转换成可观测的 object。

并且给 value 新增一个ob属性,值为该 value 的 Observer 实例。这个操作相当于为 value 打上标记,表示它已经被转化成响应式了,避免重复操作

然后判断数据的类型,只有 object 类型的数据才会调用 walk 将每一个属性转换成 getter/setter 的形式来侦测变化。 最后,在 defineReactive 中当传入的属性值还是一个 object 时使用 new observer(val)来递归子属性,这样我们就可以把 obj 中的所有属性(包括子属性)都转换成 getter/seter 的形式来侦测变化。 也就是说,只要我们将一个 object 传到 observer 中,那么这个 object 就会变成可观测的、响应式的 object。

参考文献