一、观察者模式
1.1、观察者模式概念
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
1.2、传统的观察者模式
UML类图
观察者模式的主要角色如下
- 抽象主题(Subject):即:被观察者。也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 抽象观察者(Observer):它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体主题(Concrete Subject):即:具体被观察者。也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 具体观察者(Concrete Observer):实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
1.2、前端中的观察者模式
UML类图
1.4、观察者模式的优缺点
优点: 观察者与被观察者之间是属于轻度的关联关系,并且是抽象耦合的,这样,对于两者来说都比较容易进行扩展。
缺点: 观察者模式是一种常用的触发机制,它形成一条触发链,依次对各个观察者的方法进行处理。由于是链式触发,当观察者比较多的时候,性能问题是比较令人担忧的。并且,在链式结构中,比较容易出现循环引用的错误,造成系统假死。
二、【观察者模式】和【发布订阅模式】的关系和区别
- 《Head First设计模式》里提到,发布者+订阅者 = 观察者模式。两者只是名字不太一样:发布者就是“主题”,订阅者就是“观察者”。
- 《JavaScript设计模式》中,观察者模式又被称作发布-订阅者模式或消息机制,定义了一种依赖关系,解决了主题于观察者之间功能的耦合。
以上说法都不完全正确。这只是说明了两者有相似这处,但两者并不能画等号。
2.1、关系:发布订阅模式属于广义上的观察者模式
发布订阅模式是最常用的一种观察者模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式。
2.2、区别:发布订阅模式多了事件通道(可以理解为是一个调度中心)。
在【观察者模式】中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。
╭─────────────╮ Fire Event ╭──────────────╮
│ │─────────────>│ │
│ Subject │ │ Observer │
│ │<─────────────│ │
╰─────────────╯ Subscribe ╰──────────────╯
在【发布订阅模式】中,发布者和订阅者之间多了一个发布通道,一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件。
以此避免发布者和订阅者之间产生依赖关系,从而达到完全解耦。
╭─────────────╮ ╭───────────────╮ Fire Event ╭──────────────╮
│ │ Publish Event │ │───────────────>│ │
│ Publisher │────────────────>│ Event Channel │ │ Subscriber │
│ │ │ │<───────────────│ │
╰─────────────╯ ╰───────────────╯ Subscribe ╰──────────────╯
从两张图片可以看到,最大的区别是调度的地方。
虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调度的。
从依赖关系上讲
- 所以观察者模式中的观察者和被观察者,是松耦合的关系
- 而发布/订阅模式中的- 发布者和订阅者,则完全不存在耦合。
从使用层面上讲
- 观察者模式,多用于单个应用内部
- 发布订阅模式,则更多的是一种跨应用的模式,比如我们常用的消息中间件
发布者直接触及到订阅者的操作,叫观察者模式。发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。
3、用前端代码具体实现【观察者模式】和【发布订阅模式】
一、观察者模式
在对象之间定义一个一对多的依赖,当对象自身状态改变的时候,会自动通知给关心该状态的观察者。解决了主题对象与观察者之间功能的耦合,即一个对象的状态改变给其他对象通知的问题。
主题提供维护观察者的一系列方法,观察者提供更新接口。观察者把自己注册到主题里,在主题发生变化时候,调度观察者的更新方法。
// 主题(被观察者)
class Subject {
constructor() {
this.state = 0
// 观察者列表
this.observers = []
}
// 获取状态值
getState() {
return this.state
}
// 设置状态值
setState(state) {
this.state = state
// 当自身状态改变时,通知所有观察者
this.notify()
}
// 添加观察者
add(observer) {
this.observers.push(observer)
}
// 删除观察者
remove(observer) {
let index = this.observers.indexOf(observer)
if (index > -1) {
this.observers.splice(index, 1)
}
}
// 通知所有观察者(调度观察者的更新方法)
notify() {
for (let observer of this.observers) {
observer.update()
}
}
}
// 观察者类
class Observer {
constructor(name) {
this.name = name
}
// 观察目标更新时
update() {
console.log(`${this.name}观察到目标主题状态发生了改变`)
}
}
// 主题(被观察者)
let subject = new Subject()
// 观察者
let obs1 = new Observer('观察者1')
let obs2 = new Observer('观察者2')
// 观察者把自己注册到主题里
subject.add(obs1)
subject.add(obs2)
// 主题发生变化时候,内部会通知观察者,调度观察者的更新方法。
subject.setState(1)
// 输出内容如下:
// 观察者1观察到目标主题状态发生了改变
// 观察者2观察到目标主题状态发生了改变
二、发布订阅模式
也是定义一对多的依赖关系,对象状态改变后,通知给所有关心这个状态的订阅者。发布订阅模式有订阅的动作,可以不和观察者直接产生联系,只要能订阅上关心的状态即可,通常可用第三媒介来做,而发布者也会利用第三媒介来通知订阅者。
// 发布订阅调度中心
class PubSub {
constructor() {
// 存储可以广播的主题
this.topics = {}
}
// 订阅感兴趣的事件(带有特定的主题名称和回调函数),待执行当主题/事件时被观察到
subscribe(topic, fn) {
if (!this.topics[topic]) {
this.topics[topic] = []
}
this.topics[topic].push(fn)
}
// 发布或广播感兴趣的事件(带有特定的主题名称和参数)
publish(topic, ...arg) {
for (let fn of this.topics[topic]) {
fn.call(this, ...arg)
}
}
// 退订指定主题/事件的某个回调函数或所有回调函数
unSubscribe(topic, fn) {
// 指定主题/事件的回调函数列表
let fnList = this.topics[topic]
if (!fnList) {
return false
}
// 如果没有指定回调函数,则退订所有,否则退订指定回调函数
if (!fn) {
fnList && (fnList.length = 0)
} else {
let index = fnList.indexOf(fn)
fnList.splice(index, 1)
}
}
}
const pubSub = new PubSub()
// 订阅主题/事件(带有回调函数)
pubSub.subscribe('onwork', time => {
console.log(`上班时间:${time}`)
})
pubSub.subscribe('offwork', time => {
console.log(`下班时间:${time}`)
})
pubSub.subscribe('launch', time => {
console.log(`午饭时间:${time}`)
})
// 发布主题/事件(带有参数),触发上面绑定的回调函数
pubSub.publish('onwork', '8:30')
pubSub.publish('launch', '12:00')
pubSub.publish('offwork', '17:30')
// 上班时间:8:30
// 午饭时间:12:00
// 下班时间:17:30
上面的代码就是发布订阅最重要的部分,虽然没有明显的实现观察者类和被观察者类,但观察者和被观察者在发布订阅模式中本来就不会有直接关联,都只需调用以上代码(事件总线)中的发布和订阅方法即可。