观察者模式

观察者模式:一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使其更新。

特点

  • 替代传递回调函数
  • 对象之间的通知模式

关键

  1. 发布者
  2. 发布者的缓存列表,用于缓存回调
  3. 订阅消息
  4. 发布消息,依次调用缓存回调

通用实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var event = {
eventList: new Map(),
listen (key, fn) {
const { eventList } = this
if (!eventList.get(key)) {
eventList.set(key, [])
}
eventList.get(key).push(fn)
},
trigger (key, ...others) {
const fns = this.eventList.get(key)
if (!fns || !fns.length) {
return
}
fns.forEach(fn => fn.apply(this, others))
}
}
var installEvent = obj => Object.assign(obj, event)

添加取消订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
event.remove = (key, fn) => {
const { eventList } = this
const fns = eventList.get(key)
if (!fns) {
return false
}
if (!fn) {
return eventList.set(key, [])
}
fns.splice(fns.indexOf(fn), 1)
}

实例,订阅登陆成功的事件

  1. 登陆成功为异步
  2. 成功后需要进行相应的刷新等处理,处理时不定的,可变的,后续可能会增加的
  3. 使用回调会导致强耦合,每次新增模块后,这边都需要添加一个步骤
  4. 让模块来订阅登陆成功事件即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var login = installEvent({
goLogin () {
$.ajax('url', res => {
this.trigger('loginSuccess', res)
})
}
})
var header = (function () {
login.listen('loginSuccess', header.setAvatar(res))
function setAvatar(res) {
console.log('设置头像', res)
}
return {
setAvatar
}
})()

你不必再关注为登陆成功添加回调处理,只需添加登陆成功的订阅即可。

模块间通信

发布订阅模式可以让两模块保持封装的前提下进行通信。

1
2
3
4
5
6
7
var a = (function (){
Event.trigger(‘add’, 1)
})()
var b = (function (){
Event.listen(‘add’, val => console.log(val))
})()

离线消息,实现先发布,后订阅

通过离线消息栈来保存没有被订阅的但是发生了的事件,等到有人订阅再依次取出执行。

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
Event.trigger = function (key, ...others) {
const fns = this.eventList.get(key)
if (!fns || !fns.length) {
// 若消息还没有被订阅,则先缓存起来
return this.offlineStack.push({
key,
args: others
})
}
fns.forEach(fn => fn.apply(this, others))
}
Event.listen = function (key, fn){
const { eventList } = this
if (!eventList.get(key)) {
eventList.set(key, [])
}
eventList.get(key).push(fn)
// 离线消息中有此事件,则触发消息,并删除缓存
this.offlineStack = this.offlineStack.filter(event => {
if (key === event.key) {
fn.apply(this, event.args)
return false
}
return true
})
}
}

其他语言实现

Subject 通知者

使用抽象类或者接口实现,包含以下属性和方法

  1. observer列表
  2. Attach 增加观察者
  3. Detach 删除观察者
  4. Notify 通知

Observer 观察者

在得到主题通知时更新自己。包含一个Update方法。

可以看出来,不同于javascript,其他语言的观察者往往需要实现Update接口,假设是别人已经写好的,就会出现问题了。

事件委托

针对观察者,我们可以将其Update方法改为各种不同的事件,并将其委托到Subject的Update方法上,当我们进行Notify时,调用Subject的Update方法,类似我们在js中,将回调事件替代update的方式。

推拉模型

推模型: 在事件发生时,发布者一次性把所有状态和数据都发送给观察者,类似在notify方法中直接调用我们的回调

拉模型: 在事件发生时,发布者只对观察者进行状态更新,让观察者自行实现自己的update方法。

我们可以根据实际的需求,按照自己所需进行实现。

总结

优点:

  1. 替代异步回调,时间解耦
  2. 对象解耦,模块通信

可广泛使用到MVC或者MVVM中

缺点:

  1. 时间和内存消耗
  2. 过渡使用,容易搞不清对象间的联系,不易追踪bug

引用

《JavaScript设计模式与开发实践》
《大话设计模式》