观察者模式定义
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。在观察者模式中,主体是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅并接收通知。
例如:在学习平台上预约了直播,开播就会通知已预约的人准时参加。直播是《被观察者》,预约的同学是《观察者》。
观察者模式UML类图
观察者模式角色
从类图中可以看出主要有四个角色。
- Subject(被观察者) 即被观察的抽象类,包含具体被观察对象所该实现的方法定义,有时它也被直接用作具体类。
- ConcreteSubject(具体被观察者) 是被观察者抽象类的具体实现,包含被观察的数据的状态,在数据的状态发生时,即可通知列表中的观察者。
- Observer(观察者) 观察者的接口定义。
- ConcreteObeserver(具体观察者) 具体的各个观察者类,需要实现update方法,具体被观察者将在内部数据状态改变时通知更新。
观察者模式demo
// 被观察者
abstract class Subject {
private list: IObserver[] = [];
addObserver(ob: IObserver) {
this.list.push(ob);
}
removeObserver(ob: IObserver) {
let index = this.list.indexOf(ob);
if (index > -1) {
this.list.splice(index, 1);
}
}
notifyObserver() {
for (const iterator of this.list) {
iterator.update && iterator.update();
}
}
}
// 具体被观察者
class ConcreteSubject extends Subject {
sendTask() {
console.log("公司有个项目,完成可获得无限火力卡奖励");
this.notifyObserver();
}
}
// 观察者接口
interface IObserver {
update(): void;
}
// 具体观察者A
class ObserverA implements IObserver {
update() {
console.log("ObserverA: 冲啊,卷死他们");
}
}
// 具体观察者B
class ObserverB implements IObserver {
update() {
console.log("ObserverB: 我已经有十张了,把机会让给别人吧");
}
}
// 具体观察者C
class ObserverC implements IObserver {
update() {
console.log("ObserverC: 我再集一张就能召唤神龙了");
}
}
class Main {
constructor() {
let subject = new ConcreteSubject();
let C = new ObserverC();
subject.addObserver(new ObserverA());
subject.addObserver(new ObserverB());
subject.addObserver(C);
// 你就别凑热闹了
// subject.removeObserver(C);
subject.sendTask();
}
}
new Main();
观察者模式优缺点
1. 优点
-
两者是抽象耦合的,降低了被观察者与观察者之间的耦合关系
-
建立了一套触发机制,形成一条触发链 2. 缺点
-
链式触发,在观察者多、链条长的情况下,效率会很低
-
被观察者与观察者有循环依赖的情况下,出现循环调用
与发布订阅模式有什么不同
同学A:喂,我有个问题,这个观察者模式和发布订阅模式有什么不同?我怎么感觉一毛一样好嘛。
我:问得好,度娘同学你来回答一下这个问题。
发布订阅模式UML类图
发布订阅模式对比
观察者模式和发布订阅模式本质上看起来是一样的,都是实现观察通知(订阅发布)的过程。从类图可以看出发布订阅模式多了一个调度中心,发布者和调度者不知道彼此的存在,通过调度中心统一处理,将发布者的消息通知到其订阅者。
- 发布订阅模式是一种多对多的关系
- 观察者模式是一对多的关系
- 发布订阅模式可以理解成,顾客(订阅者) - 淘宝(调度中心) - 商家(发布者)
- 观察者模式可以理解成,用户(观察者) - 微信公众号(被观察者)
发布订阅模式demo
interface Publisher {
topic: string;
data: any;
}
interface Subscriber {
param: { topic: string, callback: (data?: any) => void }[]
}
interface Channel {
publish: (topic: string, data: any) => void;
subscribe: (topic: string, callback: (data?: any) => void) => void;
unsubscribe: (topic: string, callback: (data?: any) => void) => void;
}
class ConcreteChannel implements Channel {
private subjects: { [key: string]: ((data?: any) => void)[] } = {};
public subscribe(topic: string, callback: (data?: any) => void): void {
if (!this.subjects[topic]) {
this.subjects[topic] = [];
}
this.subjects[topic].push(callback);
};
public unsubscribe(topic: string, callback: (data?: any) => void): void {
let index = this.subjects[topic].indexOf(callback);
if (index > -1) {
this.subjects[topic].splice(index, 1);
}
};
public publish(topic: string, data: any): void {
let itemA = this.subjects[topic];
console.log(data);
if (itemA) {
itemA.forEach(item => item(data));
}
};
}
class ConcretePublisher implements Publisher {
topic: string = "";
data: any;
constructor(topic: string, data: any) {
this.topic = topic;
this.data = data;
}
}
class ConcreteSubscriber implements Subscriber {
param: { topic: string, callback: (data?: any) => void }[]
constructor(param: { topic: string, callback: (data?: any) => void }[]) {
this.param = param;
}
}
class Main {
constructor() {
let userA = new ConcreteSubscriber([{
topic: "shop1",
callback: () => {
console.log("userA:等一波shop1开门")
}
}]);
let userB = new ConcreteSubscriber([{
topic: "shop1",
callback: () => {
console.log("userB:等一波shop1开门")
}
}, {
topic: "shop2",
callback: () => {
console.log("userB:再等一波shop2开门")
}
}]);
let userC = new ConcreteSubscriber([{
topic: "shop2",
callback: () => {
console.log("userC:等一波shop2开门")
}
}]);
let shop1 = new ConcretePublisher("shop1", "shop1开门了");
let shop2 = new ConcretePublisher("shop2", "shop2开门了");
let channel = new ConcreteChannel();
userA.param.forEach(item => {
channel.subscribe(item.topic, item.callback);
})
userB.param.forEach(item => {
channel.subscribe(item.topic, item.callback);
})
userC.param.forEach(item => {
channel.subscribe(item.topic, item.callback);
})
channel.publish(shop1.topic, shop1.data);
// channel.unsubscribe(userB.param[1].topic,userB.param[1].callback);
channel.publish(shop2.topic, shop2.data);
}
}
new Main()
发布订阅模式优缺点
1. 优点
-
时间解耦、发布者与订阅者双向解耦
-
易与扩展 2. 缺点
-
发布者无法获得订阅者的执行情况
同学B:啊这,这不就是事件调度管理嘛!
我:(偷偷看了Laya的EventDispatcher)你说的没错,on对应subscribe,emit对应publish,off对应unsubscribe,再加些所需的once,hasListener等等就是Laya引擎里的EventDispatcher了。
同学A:打断一下,如果我在事件未注册时,先发送了一条消息,那不就没了嘛,破玩意儿
我:(???又是这货,我怎么知道)那你先存个cacheList,等注册的时候你再取出来给人发送过去就好了。好了,今天的分享到此结束!!!(好险,还好我收的快)
思考与讨论
Mobx是通过函数响应式编程使状态管理变得简单和可扩展的状态管理库。
- 被观察者 State(存储state的store)
- 观察者 使用了Derivation的组件(Computed values、Reactions) Actions是所有会修改状态的代码,在修改了应用状态(被观察者)之后,所有的观察者都会同步执行副作用。
StateStore
// stateStore
import { observable, action, computed } from 'mobx'
class AppStore {
levelNameMap = ["", "青铜", "白银", "黄金"];
@observable achievementInfo = {
level: 1,
name: this.levelNameMap[1]
}
@computed get achievementName() {
return this.achievementInfo.name;
}
@action achievementLevelUp() {
let level = Math.min(this.achievementInfo.level + 1, this.levelNameMap.length - 1);
this.achievementInfo = {
level,
name: this.levelNameMap[level]
}
}
}
export default new AppStore();
接下来只要在组件中使用这些状态数据,并将组件设置为observer(观察者),然后在某些地方触发Action,组件就会自动响应。
import { Component } from 'react'
import { Button, View } from '@tarojs/components'
import './index.scss'
import { inject, observer } from 'mobx-react'
@inject("appStore")
@observer
export default class Index extends Component<IProps> {
render() {
return (
<View className='index-page'>
<View>{this.props.appStore.achievementName}</View>
<Button onClick={this.levelUp}>升级</Button>
</View>
)
}
levelUp = () => {
this.props.appStore.achievementLevelUp();
}
}
猜一猜MobX是观察者模式?发布订阅模式?
有人认为Mobx在广义上可以理解成(强行理解)是一个观察者模式。
(实际上作者认为:🤙🤙🏼🤙🏾Guise, #mobx isn't pubsub, or your grandpa's observer pattern. Nay, it is a carefully orchestrated observable dimensional portal fueled by the power cosmic. It doesn't do change detection, it's actually a level 20 psionic with soul knife, slashing your viewmodel into submission.)