沐光

记录在前端之路的点点滴滴

分析 Vue 的观察者模式(中)

前言

在上篇分析 Vue 的观察者模式(上)中,简单实现了“手动”版观察者模式,而在本篇中我们继续沿着该思路往下探索,看如何实现“自动”版的观察者。

完善的观察者

紧接着上文的思路,因为我们只是实现了 Dep 依赖(管家),却还没有对平台的观察处理方式优化,因此我们需要将此步骤给完善起来。

首先,要实现发布者更新内容后自动触发订阅事件,那么我们需要的是对发布者内容改变的监听,ES5 中恰好有这么一种方法能满足我们的需求,那就是 Object.defineProperty(...)。其语法很简单,我们此处仅仅需要拦截 get 和 set 方法,大致写法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const data = {
name: 'test'
};

Object.defineProperty(data, name, {
get() {
return data.name;
},
set(newVal) {
// 调用 setter 方法,不可直接赋值,否则会死循环
// 错误写法 obj[key] = newVal
// 可查看 Vue 中的 setter 获取
}
})

既然有了能够观测发布者的方法,那么对于对象变更后的监听就能自动化处理了,那么先前手动触发 dep.depend()dep.notify() 函数的操作就能整合入 Object.defineProperty 内了。

延续先前的逻辑,dep.notify() 方法执行都得在对象值变动之后,因此我们可以将此方法放入 set 函数内;而 dep.depend() 方法主要是将订阅者事件存储入自身的事件列表中,因此该方法执行一次就行,之后每次更新都触发 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
43
44
45
// (私人管家)依赖收集
class Dep {
constructor() {
this.subscriberList = [];
}

// 添加订阅方法
depend() {
// 此处添加对应订阅方法
}

// 发布者更新消息
notify() {
this.subscriberList.forEach(sub => {
// 依次处理订阅的方法
});
}
}

// (发布者和管家的联系方式)监听器
function observer(data = {}) {
Object.keys(data).forEach(key => {
let val = data[key];
const dep = new Dep();

Object.defineProperty(data, key, {
get() {
// get 时添加依赖,当有目标的时候才添加
if (target) {
dep.depend();
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
// set 时触发更新
// 此处用 setter 做变动,此处偷懒一下,不要直接赋值,会反复触发 set
val = newVal;
dep.notify();
}
})
});
}

那么现在问题又有了。目前代码实现至此我们仅仅只是将发布者与其 Dep 依赖进行了关联,那么观察者又该如何将其获订阅者的事件与 Dep 存储的事件列表进行关联呢?从订阅者角度分析一下:

  1. 订阅者决定好要订阅的内容(订阅者的函数);
  2. 订阅者在对应的观察者平台上注册事件,绑定发布者信息;
  3. 观察者先联系到 Dep 管家,将订阅事件存放至管家处;
  4. 待管家拿到了发布者新情报,则响应所有存储好的订阅者的订阅事件了。

思路逐渐清晰,那么这么个流程我们可以简化为如下代码:

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
target = null;

class Dep {
constructor() {
this.sub = [];
}
depend() {
if (target && !this.sub.includes(target)) {
this.sub.push(target);
}
}
notify() {
this.sub.forEach(cb => cb());
}
}

function watcher(callback) {
target = callback;

// 此处触发一下监听对象的 get 方法,将 callback 方法加入 Dep 依赖的订阅队列中
// 然后清除掉 target,防止重复注册

target = null;
}

watcher(() => {
// 此处是订阅事件
})

这样我们就讲观察者的订阅方法和 Dep 依赖给绑定上了。考虑到 watcher 的通用性,这里写了一个加强版的 demo,如下:

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
let target = null;

// (私人管家)依赖收集
class Dep {
constructor() {
this.subscriberList = [];
}

// 将当前的 watcher 加入 dep 中
addSub(watcher) {
this.subscriberList.push(watcher)
}

// 添加订阅方法
depend() {
if (target) {
target.addDep(this);
}
}

// 发布者更新消息,触发所有的订阅方法
notify() {
this.subscriberList.forEach(sub => {
sub.update();
});
}
}

// (平台)观察者
class Watcher {
constructor(data = {}, key = '', cb = () => {}) {
this.cb = cb;
this._data = data;
this.key = key;

target = this;
// 触发 getter,存储本 watcher
this.value = data[key];
// 防止反复触发
target = null;
}

addDep(dep) {
dep.addSub(this);
}

update() {
const newVal = this._data[this.key];
this.value = newVal;
this.cb(newVal);
}
}

// (发布者和管家的联系方式)监听器
function observer(data = {}) {
Object.keys(data).forEach(key => {
let val = data[key];
const dep = new Dep();

Object.defineProperty(data, key, {
get() {
if (target) {
// get 时添加依赖
dep.depend();
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
// set 时触发更新
val = newVal;
dep.notify();
}
})
});
}

// 发布者
const publisher = {
bookName: 'book',
bookContent: 'hello world'
}

// 管家开始观测发布者
observer(publisher);

// 订阅者在平台上订阅发布者的部分信息
new Watcher(publisher, 'bookName', name => {
console.log(`new book name is ${name}`);
});
new Watcher(publisher, 'bookContent', content => {
console.log(`new book content is ${content}`);
});

// 发布者发布信息
publisher.bookName = 'new book';
publisher.bookContent = 'new content';

Vue 观察者的实现

在看源码之前,首先让我们看看 Vue 的实现原理图:

Reactive

先仅考虑 data 部分的观察者模式,我们可以看到大致步骤为:

  1. 为发布者 data 做数据监听处理;
  2. 为劫持方法添加 Dep 依赖收集,get 存储 target 目标,set 设置 target 目标;
  3. 依赖收集触发后,触发 Watch 观察者,通知订阅者改变。

因为流程图中只是详细展示了响应式部分的变动,为了便于大家更清楚的理解这部分的流程,所以这里先解释一下 Vue 中完整的流程:

  1. 页面开始渲染,对 data 进行依赖收集;
  2. 依赖收集完毕,开始监测订阅者的方法,并存储原值方便变更时的对比;
  3. 因为依赖收集完毕,要存储原值则触发了 get 事件,此时添加 dep 依赖,将对应生成的 watcher 存储入依赖列表内
  4. 页面渲染完成,用户触发页面某些点击事件(比如按钮);
  5. 点击事件绑定了某个 methods 方法,该 methods 方法使得 data 部分内容更新了;
  6. 因为已经对 data 进行了依赖收集,触发了对应的 set 方法;
  7. set 方法对比新旧值发现值更新了,此时重新收集依赖新的值(特别是对象的情况),然后通知它的私有管家 Dep 进行更新;
  8. Dep 依赖开始遍历其所存储的数组,为该发布者的订阅者们发送消息,触发订阅事件。
  9. 各订阅者事件触发 update 更新,此时便流转到 render 渲染,之后页面就更新啦!

为了方便大家理解源码,这里手动实现了一个简单的 observer 模式,基本是按着源码的思路一点点撸出来的,注释很全,方便大家理解!

源码地址

所有的 demo 都写入仓库了,仓库地址为:传送门