前言

《JavaScript设计模式与开发实践》一书中说分辨模式的关键是意图而不是结构。在意图方面上说,这两种模式的意图都是定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新;而从结构方面来说,观察者模式是观察者和被观察者(目标对象)之间的通讯,两者是直接关联的,而发布-订阅模式是发布者和订阅者之间的通讯,但是两者不是直接关联的。

一、观察者模式

观察者模式就是观察者和被观察者之间的通讯。描述的是对象间的一种一对多的关系,即一个或多个观察者对目标对象的状态感兴趣,通过将自己本身依附在目标对象上以便关注所感兴趣的内容。目标对象的状态发生改变,若观察者对这些改变感兴趣,会发送一个通知消息,调用每个观察者的更新方法,当观察者不再对目标状态感兴趣时,他们可以简单将自己从中分离。举个例子:

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
class ObservedTarget {
constructor() {
this.observers = [] //用于存储所有的观察者
}
addObserver(...observer) {
// console.log(...observer);
this.observers.push(...observer) //添加观察者
}
notifyObserver(...args) {
// 遍历观察者列表
this.observers.forEach(item => {
item.update(...args)
})
}
}
class Observer {
constructor(name) {
this.name = name
}
update(...args) {
let content = [...args];
console.log(`${this.name}接收到目标对象更新的状态是:${content}`);
}
}
// 创建多个观察者
let observer1 = new Observer('observer1')
let observer2 = new Observer('observer2')
let observer3 = new Observer('observer3')
// 把观察者本身依附在目标对象上
let observerTarget = new ObservedTarget()
observerTarget.addObserver(observer1, observer2, observer3)//直接关联
// 当目标对象更新内容时,通知所有的观察者
observerTarget.notifyObserver('这是我的新专辑!', '感谢粉丝对我的支持呀!')
//observer1接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
//observer2接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
//observer3接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!

以上不难看出,多个观察者将自己本身依附在目标对象上,这样就可以做到直接相关联,当目标对象的状态发生改变时,通知消息(广播出去),那么所有的观察者就都可以得到最新的消息了。对于学习vue的胞友来说,这个vue中双向绑定原理就是观察者模式的应用之一,可以看看双向绑定的原理:

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 currentEffect = null;//在这里,通过这个全局变量将观察者和目标对象进行关联
class Dep {//就是目标对象
constructor(val) {
this._val = val
this.effects = new Set() //存储依赖,依赖只收集一次
}
get value() { //get操作
// 读取
this.depend() //每次读取都会触发依赖收集
return this._val
}
set value(newVal) { //set操作
// 修改
this._val = newVal
this.notify() //值更新完毕后,通知更新
return this._val
}
depend() { //收集依赖
// 收集依赖时,需要先将收集的依赖存储起来,而且不重复收集依赖
// 依赖是通过effectWatcher内部的回调函数配合effectWatcher实现的,所以需要关联到effectWatcher函数,可以先定义一个全局变量currentEffect
if (currentEffect) {
this.effects.add(currentEffect)
}
}
notify() { //通知更新
// 遍历所有依赖并执行
this.effects.forEach(effect => {
effect()
})
}
}
// effect函数
function effectWatcher(effect) {//就是个观察者
currentEffect = effect //每收集一个依赖,都会关联到depend函数
effect() //保证一上来就执行
currentEffect = null
}
// 使用
const dep = new Dep('没有任何最新的动态~')
let content;
effectWatcher(() => {
content = dep.value
console.log(content);
})
// 当值发生改变
dep.value = '目标对象发布新专辑了!'
// 没有任何最新的动态~
// 目标对象发布新专辑了!

以上不难看出,Dep就充当目标对象,在vue中是依赖收集者;而就是effectWatcher观察者的身份了,在vue中是effect函数;其实这个vue的双向绑定原理的优化在于,我不再需要手动调用更新去广播,只要目标对象的值发生改变,会自动去广播。其实这就是观察者模式!

二、发布订阅模式

发布-订阅模式也是通过⼀对⼀或者⼀对多的依赖关 系,当对象发⽣改变时,订阅⽅都会收到通知。在现实⽣活中类似场景是相当的多,⽐如公众号的订阅,预售消息订阅等等。

但是,相比于观察者模式,发布订阅模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝订阅者和发布者的依赖关系,直白地说,订阅者只是订阅自己感兴趣的内容,并不关心目标对象的存在;而目标对象也只是发布自己想发布的内容,并不关心订阅者的具体存在。发布者和订阅者并无直接联系。

再回到观察者模式,它有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加(依附)到目标对象中进行管理;另一方面,目标在触发事件的时候,无法将通知操作委托给事件通道,而只能亲自去通知所有的观察者。

1. 例子

实际上,在生活中处处都有发布-订阅的存在,只要我们曾经在DOM节点上面绑定过事 件函数,那我们就曾经使用过发布—订阅模式,只是你不知道而已,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="box"></div>
<script>
function click() {
console.log(3);
}
let box = document.getElementById('box')
box.addEventListener('click', () => {
console.log(1);
})
box.addEventListener('click', () => {
console.log(2);
})
box.addEventListener('click', () => {
click()
})
box.click()//模拟用户点击
</script>

当我点击box时,会同时打印1,2,3,如图所示:

2. 实现原理

其实,除去DOM事件,常用的还是自定义的其他事件,经过时间的洗礼,发布-订阅模式不再是简单的DOM事件可以体现的,现如今的发布-订阅模式除了发布、订阅,还有只订阅一次和取消订阅,在写发布-订阅模式之前呢,还需要知道一些关键的人物,下手才能行云流水,正所谓磨刀不误砍柴工:

  • 确认发布者的职责,发布者需要一个缓存列表,用于存放所有的订阅者回调函数,以便消息通知;在发布时,发布者会遍历该缓存列表,依次触发存放的订阅者回调函数。
  • 确认订阅者的需求,订阅者可以接收一些参数,这些参数是来自回调函数的,这些参数包括订阅者感兴趣内容相关的其他信息,当然,订阅者可以自行处理这些参数。
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
class PubSubTopics {//调度中心
constructor() {
this.subscribers = [] //用于存储所有的订阅者回调函数
}
subscrible(message, callback) { //订阅
let callbacks = this.subscribers[message]
if (!callbacks) { //不存在
this.subscribers[message] = [callback]
} else {
callbacks.push(callback)
}
}
publish(message, ...args) { //发布
let callbacks = this.subscribers[message] || []
callbacks.forEach(callback => callback(...args))
}
once(message, callback) { //只订阅一次
let onceFn = (...args) => {
callback.apply(this, args) //当场执行,不进入缓存列表
this.remove(message) //再取消订阅
}
this.subscrible(message, onceFn)
}
remove(message, callback) { //取消订阅
let callbacks = this.subscribers[message] || []
if (!callback) { //没有传入具体的回调函数,则取消对应的所有订阅
callbacks && (callbacks = [])
} else {
callbacks.forEach((cb, index) => {
if (cb == callback) { //具名函数
callbacks.splice(index, 1)//删除
}
})
}
}
}
//测试
let subscriberA = new PubSubTopics()
subscriberA.subscrible('song', (song) => {
console.log('new song=', song);
})
subscriberA.subscrible('teleplay', userA = (teleplay) => {
console.log('new teleplay=', teleplay);
})
subscriberA.subscrible('teleplay', userB = (teleplay) => {
console.log('new teleplay=', teleplay);
})
subscriberA.subscrible('teleplay', userC = (teleplay) => {
console.log('new teleplay=', teleplay);
})
subscriberA.subscrible('movie', (movie) => {
console.log('new movie=', movie);
})
// 取消订阅
subscriberA.remove('teleplay', userA)
subscriberA.remove('teleplay', userC)
// 只订阅一次
subscriberA.once('movie', (...args) => {
console.log('我只订阅一次:', args);
})
subscriberA.publish('song', '李荣浩 乌梅子酱')
subscriberA.publish('teleplay', '张译 他是谁')
subscriberA.publish('movie', '易烊千玺 满江红')
//new song= 李荣浩 乌梅子酱
//new teleplay= 张译 他是谁
//new movie= 易烊千玺 满江红
//我只订阅一次: [ '易烊千玺 满江红' ]

至此,一个发布-订阅模式就大致实现了,其实在vue中,$on、$emit、$off、$once组合起来可不就是一个发布订阅模式了嘛。

三、总结

1. 观察者模式

观察者模式包括两个主体:观察者和被观察者(目标对象),它属于行为型模式(关注对象之间的通信),也是一种将代码解耦的设计模式,观察者不需要直接调用被观察者的内部方法或属性获取通知。

观察者模式描述的是对象之间一种一对一或一对多的关系,观察者需要将自己注册(依附)在被观察上。

2. 订阅发布模式

发布-订阅模式包括三个模块:发布者、订阅者和调度中心(办事大厅),发布者和订阅者两者之间是分离的,即订阅者只管在调度中心订阅,有人调用才响应;而发布者只管在调度中心广播。而且,在事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者。

发布-订阅模式的优点在于两个方面:一是时间上的解耦;而是对象之间的解耦。可以用在异步编程中,以便满足更加松耦合的需要。但是,万物都有两面性,发布订阅模式也不例外,该模式虽然可以弱化对象之间的耦合度,但是也不便于过渡使用,否则会难以跟踪和理解;另外,缓存大量的订阅者也需要消耗一定的时间和内存,一般是先订阅后发布,所以有些时候订阅者也许会永远存在内存中,但是这个消息可能始终未发生。

发布订阅模式和观察者模式相比多了一个事件的调度中心,而观察者模式是两者直接关联的。

vue的双向绑定原理就是观察者模式。