沐光

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

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

前言

最近又鼓起勇气鼓捣 vue 源码,然后不经意间瞅了瞅最早开始学习 vue 源码时跟着撸的一个 vue-observer 的实例,发现自己对于观察者模式还是有些不太明白。因此,在学习了《JavaScript 设计模式》的“观察者模式”一篇后,总算有了一点灵感。目前趁灵感还在,在此便记录下我的理解。

观察者模式

什么是观察者模式呢?首先引用一下比较正规的说法:

观察者模式:又被称作发布-订阅者模式或消息机制,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合。

简单来说,观察者模式主要实现的是一种对于未来可能发生的事情做的一种消息订阅处理。DOM2 级事件中的 addEventListener 方法以及生活中的订报纸都基本上就是这种原理。

首先我们得有发布者和订阅者,他们之间如何联系起来那就需要一个观察者平台。订阅者在观察者平台上向发布者添加订阅,待发布者在发布信息后,观察者平台获取到更新的信息后,给各个订阅者发提醒表示“你关注的发布者更新了文档哦~”,这样订阅者就能得到更新后的消息内容了。

其实发布者和订阅者都很简单,关键点还是观察者这个中间平台的实现。我们最终想要的结果其实可简化为如下代码:

1
2
3
4
5
6
7
8
9
10
// 观察者(平台)
function observer() { /***/ }

// 发布者(平台作者)
const publisher = { bookName: 'Hello World!', content: 'This is a book' }

// 订阅者(在平台上订阅某作者)
const subscriber = observer(publisher);

// 之后 publisher 改变什么东西,observer 触发订阅的消息即可。

简单的观察者

前面分析到,观察者其实就是对于发布者和订阅者的一个中间平台,因此观察者所需要的能力包括:添加订阅者、消息推送和删除订阅者。先不考虑最终自动化处理发布订阅的流程以及删除操作,手动处理的大致流程为:

  1. 平台上新增了一个订阅者,并订阅了一个发布者;
  2. 发布者更改了自身的某些属性;
  3. 平台通知订阅者,“你订阅的发布者更新了新内容”。

因此我们可以简化一个观察者模型为:

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
class Observer {
constructor() {
this.subscriberList = [];
}

// 添加订阅方法
addSubscriber(subscribFunc) {
if (subscribFunc && !this.subscriberList.includes(subscribFunc)) {
this.subscriberList.push(subscribFunc);
}
}

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

// 观察者
const observer = new Observer();
// 发布者
const publisher = { bookName: 'Hello' }
// 订阅者
const subscriber = function () {
console.log(`The publisher your followed update the book ${publisher.bookName}`);
}

// 订阅者订阅事件需要平台完成
observer.addSubscriber(subscriber);

// 发布者更新消息
publisher.bookName = 'Hello World';

// 平台根据订阅者设置的事件通知订阅者,你有新订阅信息
observer.notify();

稍复杂的观察者

前面基本上勾勒出了基本的观察者模型大致逻辑,虽然是纯“手动”操作,但是顺着这个机制走下去就没有太大问题。现在我们需要的是慢慢将“手动”转变为“自动”,而要实现这种转变则需要一些中间的封装,也就是中间的依赖类,此时我们的思路是这样的:

  1. 为每个发布者生成一个对应的依赖收集对象(类比 Vue 中 data 返回的对象)
  2. 为负责依赖收集的对象添加订阅者事件(类比 Vue 中的 methods 和 computed 内容)
  3. 发布者更新内容,订阅者事件触发

因为待依赖收集的对象总是动态变动的,因此我们还得维护一个全局的 target 对象来动态绑定当前变动的发布者,此时的代码可以抽象为:

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

class Dep {
constructor() {
this.subscriberList = [];
}

// 添加订阅方法
depend() {
if (target && !this.subscriberList.includes(target)) {
this.subscriberList.push(target);
}
}

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

// 中间依赖的平台
const dep = new Dep();
// 发布者(data 对象中的属性,为了方便处理,直接扩出来了,写成对象也行)
let bookName = "Hello World";
let bookContent = "This is a book";
let publishInfo = ''; // 消息提示
// 绑定当前的订阅者(method 方法)
const target = function () {
publishInfo = `The book ${bookName}‘s content is ${bookContent}`;
}

// 订阅者还未订阅信息
console.log(publishInfo); // ‘’

// 订阅者在平台上添加订阅事件,并推送用户目前的最新信息
dep.depend();
target();
console.log(publishInfo) // The book Hello World‘s content is This is a book

// publisher 更新消息
bookContent = 'The content is "Hello World"';
// 平台还未处理前,原信息没有改变
console.log(publishInfo) // The book Hello World‘s content is This is a book

// 平台观察到 publisher 更新内容了,更新了用户的通知信息
dep.notify();
// 呈现更新后的信息
console.log(publishInfo) // The book Hello World‘s content is The content is "Hello World"

与前面的相比,目前这一版的优点在于:

  1. 每个发布者都有属于自己的小管家,而不是与各订阅者一一绑定(原绑定方式:发布者 -> 平台 -> 订阅者),方便消息推送管理;
  2. 平台能动态绑定发布者(原来是通过订阅者显示绑定发布者);

但是目前还有一些缺陷,那就是:还没有实现动态的观察变化

参考文章