# 造轮子 - 双向数据绑定
# 监听器 Observer 的实现
大名鼎鼎的 Object.defineProperty
相读者并不陌生,忘记的点击传送门 (opens new window)哦。
在 Observer
中其实就是利用了上述 API 来给 vm
中需要观测的属性添加了取值器 getter
, 以及存值器 setter
, 数据劫持就是那么回事儿。
# 简单看下 defineProperty()
下面先使用 Object.defineProperty
来简单观测一个对象。
let name = 'tom';
let person = {};
Object.defineProperty(person, name, {
get() {
console.log('name属性被读取了...');
return val;
},
set(newVal) {
console.log('name属性被修改了...');
val = newVal;
}
})
console.log(person);
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
这样,就给原来的空对象 person
添加了新属性 name
, 并为其添加了存值器与取值器。读取 name
时触发取值器,修改 name
时触发存值器。
# 封装一个函数
上述手段并不高明(shēng dòng),当需要观测的属性一多,就需要一个个添加,不如封装个方法。
比如员工跟老板存一家银行,老板账户里有大把 money, 员工账户里 ...(暗示 zhǎng gōng zī)。一般银行都有短信服务,这个不多说了,直接看代码。
// 一家银行的几个账户
const myBank = {
account1: '$1000000',
account2: '$1',
account3: '$1',
account4: '$1',
account5: '$1'
};
// 专为银行提供短信服务的供应商
function bankSMS(obj) {
if (!(obj instanceof Object)) {
return;
};
const keysArr = Object.keys(obj);
keysArr.forEach((key) => {
addSMS(obj, key, obj[key]);
});
return obj;
};
// 为该账户添加短信服务
function addSMS(obj, key, val) {
Object.defineProperty(obj, key, {
get () {
console.log(`账户: ${key}; 余额: ${val} --- 主人~您还养的起我嘛?`);
return val;
},
set (newVal) {
console.log(`账户: ${key} --- 小金库被动了呢!`);
return val = newVal;
}
});
};
// 使用
bankSMS(myBank);
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
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
结果分析:

读取对象 myBank
被观测属性时触发取值器;当其中一个员工实现一个“小目标”时, 触发存值器。这样对象里面的属性都是可观测的了。
# Vue 源码
什么?上面的还满足不了你?你这个年轻人不讲“武德”,那手写下 Vue 源码吧。
1. 设计思路

2. 代码
// 首先定义一个 Observer 类,传入原始数据对象,用来生成属性可被观测的实例
export class Observer {
// 生成实例
constructor (value) {
// 1. 先将原数据对象的值拷贝到实例上
this.value = value;
// 给 value 添加一个 __ob__ 属性,值为 Observer 实例,可以认为指向实例
// 作用是给 value 打一个标记,来表示此值是响应式的了,避免重复操作。
// def(value, '__ob__', this);
// 2. 然后在实例上,将每个属性都变成可观测的
// 2.1 判断如果 value 为数组,我们之后再分析,先看 value 为对象的情况
if (Array.isArray(value)) {
// ... 下面我直接 retun 了
return;
} else {
// 2.2 不是数组,执行实例原型上的 walk 方法
this.walk(value);
}
}
// 2.2 walk 方法,挂载到 Observer 的原型上
// 作用: 遍历实例上的所有属性,并为其添加响应式功能
walk (obj) {
// 2.2.1 判断是否为对象,源码用的 ts 更简洁
if (!(obj instanceof Object)) {
return;
}
// 2.2.2 利用 Object.keys() 方法提取实例上所有 key 到一个数组
const keys = Object.keys(obj);
// 遍历数组,给每一个属性添加响应式功能
for (let i = 0; i < keys.length; i++) {
defineReactive (obj, keys[i]);
};
}
}
// definReactive 方法
// 功能:调用 Object.defineProperty(),给对象中的属性添加 getter setter,使其变得可观测。
function defineReactive (obj, key, val) {
// 实参如果只传了两个,手动设置键值 val
if (arguments.length === 2) {
val = obj[key];
}
// 如果该属性为一个引用类型,递归调用 new Observer,使实例中属性值为对象的内部属性,也变得可被观测
if (typeof val === 'Object') {
new Observer(val);
}
// 调用 Object.defineProperty(), 给对象中的属性添加 getter setter,使其变得可观测
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
console.log(`账户: ${key}; 余额: ${val} --- 主人~您还养的起我嘛?`);
return val;
},
set (newVal) {
if (val === newVal) {
return;
}
console.log(`账户: ${key} --- 小金库被动了呢!`);
val = newVal;
}
});
}
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
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
# 订阅器 Dep 的实现
Dep
订阅器,用于收集 Watcher
, 对监听器 Observer
和 订阅者 Watcher
进行统一管理。
即:
- 每一个被
Observer
管理的可观测属性都拥有属于自己的dep
。 dep
中有一个属性为依赖于该可观测属性的Watcher
构成的数组- 当取用和修改可观测属性时,会通过
dep
中不同的函数做出不同的处理,比如为可观测属性添加watcher
、通知watcher
做出视图更新动作等。
// 为可观测的数据属性,构建了 dep 类,其示例为用于存储依赖于这个属性的 Watcher 构成的数组。
export class Dep {
constructor () {
subs: []
}
// 添加 Watcher
depend () {
if (window.target) {
this.addSub(window.target);
}
}
addSub (sub) {
this.subs.push(sub);
}
// 删除 Watcher
removeSub (sub) {
// 调用 remove()
remove(this.subs, sub);
}
// 通知 Watcher 数组中的元素更新
notify () {
let subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
// remove() 用于删除指定索引的数组元素。
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
arr.splice(index, 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
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
构建完成之后,需要在 Observer
中使用
get ()
中使用dep.depend
=> 为其添加watcher
set ()
中使用dep.notify
=> 通知watcher
更新
function defineReactive (obj, key, val) {
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === 'Object') {
new Observer(val);
}
// 实例化一个 Dep 订阅器
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
console.log(`账户: ${key}; 余额: ${val} --- 主人~您还养的起我嘛?`);
// 在 getter 中收集 watcher
dep.depend();
return val;
},
set (newVal) {
if (val === newVal) {
return;
}
console.log(`账户: ${key} --- 小金库被动了呢!`);
// 在 setter 中通知 watcher 更新
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
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
# 订阅者 watcher 的实现
【TODO】