发布订阅模式可以看作是观察者模式的一种升级版本,和观察者模式在结构上比较相似,但是它们在进行消息传递的时候,传递方式会有所不同。
- 观察者模式
直接通信:在观察者模式中,观察者(Observer)直接订阅主体(Subject)。当主体发生改变时,它直接通知这些观察者。
双向依赖:观察者需要知道主体,主体需要维护一个观察者列表
主体状态:观察者通常对主体的状态变化感兴趣,它们可能会从主体中检索状态。
实例:很多GUI工具使用观察者模式,例如,一个按钮(主体)可以有多个事件监听器(观察者)
- 发布订阅模式
间接通信:发布-订阅模式使用一个称作代理(Broker)或消息中介(Message Queue)的第三方组件,发布者(Publisher)不直接发送消息给订阅者(Subscriber),而是通过消息中介
解耦:发布者和订阅者不需要知道对方的存在。它们只需要知道相关的消息或事件类型。
异步通信:通常发布-订阅模式支持异步处理,订阅者可以在未来的任何时间点处理收到的消息
动态关系:订阅者可以根据需要随时订阅或取消订阅消息。
实例:消息队列(如 RabbitMQ)、流处理(如 Kafka)和许多现代应用框架使用发布-订阅模式
1.先来看一个简单的例子
javascript
class EventBus{
constructor(){
this.events = {};
}
on(event,callback){
this.events[event] = this.events[event] || [];
this.events[event].push(callback);
}
emit(event,data){
(this.events[event] || []).forEach(cb=>cb(data));
}
}
// 使用
const bus = new EventBus();
bus.on('message',data=>{
console.log('receive---',data)
})
bus.emit('message','hello,syt');
2.写一个ts版本的例子
下面这是一段伪代码,
typescript
// request.ts文件
import axios,{ AxiosResponse } from 'axios';
import router from './router';
import { message } from 'ant-design-vue';
const ins = axios.create({
baseURL:'http://localhost:3000',
});
const successHandler = (res:AxiosResponse):any=>{
//略
}
const errorHandler = (error:any):any =>{
if(error.response.status===401){
//在失败的拦截器里面如果是401的,登陆过期,
// 有一个提示弹窗
message.error('登录失败,请重新登录');
// 有一个路由跳转,逻辑是通的,但是这种写法不好,
// 因为这个模块时在做网络请求的,网络的模块里面为啥会有组件库的依赖呢,网络里面为啥
// 会对路由有依赖呢,这就很奇怪,这种一来就会造成耦合,就是网络跟我们的组件,界面耦合了,以及路由耦合了
// 那么耦合会带来什么问题呢,耦合带来的问题一定是,你耦合了什么东西,那么你耦合的东西一变,你这里很有可能会跟着变化,
// 这就会导致一个诡异的现象,将来界面需求变化了,你要去动网络
// 比如界面的逻辑不仅仅要处理401还要处理400的逻辑,
// 也有可能将来有一天登录失败不跳转到登录页面,而是弹出一个弹出层,在弹出层上登录,那这个问题就更加麻烦了
// 那么这个耦合可能以后会越来越高,直到最后你会发现这个模块10%是在处理网络,90%是在处理路由和界面,那就完全没法看
// 也就意味着你的工程将来很难维护
router.push('/login');
}
}
ins.interceptors.response.use(successHandler,errorHandler);
那么我们需要解藕,那么怎么做呢,那就是加中间层,我们先把他叫事件中心,一个模块发生了一件事,我不知道干嘛,比如发生了401了,你不知道干嘛也不要自作主张,这个事发生了,就抛出事件,让其别的模块监听事件,别的模块监听到事件就该干嘛干嘛,该是路由做的事情,就由路由去做,该是界面做的事就由界面去做,这样就解藕了,然后我们定义一个公共模块,即事件中心
typescript
// eventEmitter.ts文件
const eventNames=['API:UN_AUTH','API:INVALID'];
type EventNames = (typeof eventNames)[number];
class EventEmitter {
private listeners:Record<string,Set<Function>> = {
'API:UN_AUTH':new Set(),
'API:INVALID':new Set(),
};
on(eventName:EventNames,listener:Function){
this.listeners[eventName].add(listener);
}
emit(eventName:EventNames,...args:any[]){
this.listeners[eventName].forEach(listener=>listener(...args));
}
}
export default new EventEmitter();
然后这个网络模块里面就不要咬和界面以及路由耦合到一起了,直接引入事件中心,通过它来发送消息
typescript
// request.ts文件
import axios,{ AxiosResponse } from 'axios';
import emitter from './eventEmitter';
const ins = axios.create({
baseURL:'http://localhost:3000',
});
const successHandler = (res:AxiosResponse):any=>{
//略
}
const errorHandler = (error:any):any =>{
if(error.response.status===401){
emitter.emit('API:UN_AUTH')
}else if(error.response.status===400){
emitter.emit('API:INVALID')
}
}
ins.interceptors.response.use(successHandler,errorHandler);
将来有一天路由要处理的话,那就导入事件中心,自己去处理自己该干的事,比如跳转路由,界面也是一样的,各自模块各自处理自己的事情
typescript
//router.ts
import {createRouter,createWebHashHistory} from 'vue-router';
import emitter from './eventEmitter';
const router = createRouter({
history:createWebHashHistory(),
routes:[],
});
// 注册事件
emitter.on('API:UN_AUTH',()=>{
router.push('/login')
});
export default router;
3.demo3
typescript
// 发布者接口
interface IPublisher {
// 发布者只管发布消息
publish:(topic:string,message:string)=>void;
}
// 订阅者接口
// 现在是由订阅者自己来决定订阅哪个主题
interface ISubscriber {
subscribe:(topic:string) => void;
unsubscribe:(topic:string) => void;
receive:(message:string) => void;
}
// 中间人接口
interface IBroker {
// 订阅主题
subscribe:(topic:string,subscriber:Subscriber)=>void;
// 取消订阅
unsubscribe:(topic:string,subscriber:Subscriber)=>void;
// 发布消息
publish:(topic:string,message:any)=>void;
}
// 中间人
// 回头发布者要发布消息,就会通知中间人,中间人再通知所有订阅者
// 并且订阅者的列表也是由中间人维护的
// 中间人要做的事情:1.帮发布者发布消息2.帮订阅者订阅主题消息
class Broker implements IBroker {
// 内部维护一个主题和订阅者的列表
private topics: Map<string, ISubscriber[]> = new Map();
// 订阅方法
subscribe(topic: string, subscriber: ISubscriber):void {
// 拿到对应的主题的观察者列表(订阅者列表)
const subTopicSubscribers = this.topics.get(topic) || [];
subTopicSubscribers.push(subscriber);
this.topics.set(topic,subTopicSubscribers);
};
// 取消订阅方法
unsubscribe(topic: string, subscriber: ISubscriber):void {
const subTopicSubscribers = this.topics.get(topic)||[];
const index = subTopicSubscribers.indexOf(subscriber);
if(index!==-1){
subTopicSubscribers.splice(index,1);
}
};
// 发布消息方法
publish (topic: string, message: any):void {
// 获取到订阅了这个主题的订阅者列表
const subTopicSubscribers = this.topics.get(topic) || [];
// 遍历所有的订阅者,通知他们
for(const subscriber of subTopicSubscribers){
subscriber.receive(message);
}
};
}
//订阅者
class Subscriber implements ISubscriber {
private id:number;
private broker:IBroker;
constructor(id:number,broker:IBroker){
this.id = id;
this.broker = broker;
}
subscribe(topic: string):void{
// 订阅者要订阅哪一个主题,向中间人报名
this.broker.subscribe(topic,this);
};
unsubscribe(topic: string):void {
// 订阅者要取消订阅哪一个主题
this.broker.unsubscribe(topic,this);
};
receive (message: string):void {
console.log(`订阅者${this.id}接收到消息:${message}`);
};
}
// 发布者
class Publisher implements IPublisher {
private broker:IBroker;
constructor(broker:IBroker){
this.broker = broker;
}
// 发布消息的方法
// 要发布具体的消息,我们只需要将消息交给中间人即可
publish(topic:string,message:string):void {
console.log(`发布者发了一个${topic}主题的消息:${message}`);
// 通知中间人向指定主题发布消息
this.broker.publish(topic,message);
}
}
// 使用
// 创建一个中间人
const broker = new Broker();
// 创建一个发布者
const publisher = new Publisher(broker);
// 创建四个订阅者
const subscribe1 = new Subscriber(1,broker);
const subscribe2 = new Subscriber(2,broker);
const subscribe3 = new Subscriber(3,broker);
const subscribe4 = new Subscriber(4,broker);
// 四个订阅者订阅主题
subscribe1.subscribe('动作片');
subscribe2.subscribe('动作片');
subscribe3.subscribe('恐怖片');
subscribe4.subscribe('恐怖片');
publisher.publish("恐怖片","恐怖片上映了");
subscribe3.unsubscribe("恐怖片");
publisher.publish('恐怖片','咒怨终结版上映了')
非原创,来源渡一袁老师和谢杰老师,简单记录下吧