简介
概念
在24种基本设计模式中,并没有发布订阅模式,前期发布订阅模式只是观察者模式得到一个别称,因为观察者模式自身的一些缺点,渐渐的衍生出了观察者模式;
发布订阅模式其实就是一种对象间一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都将得到状态改变的通知;其包含
订阅者 Subscriber、发布者 Publisher和事件调度中心 Event Channel;
简单案例分析
电商-降价通知系统
淘宝就是一个
调度中心 Event Channel,客户是订阅者 Subscriber,商家就是发布者 Publisher,当客户订阅了一件商品的降价通知时,这个事件会被保存到淘宝中,当商家降价了某个商品时,相当于发布了商品降价的这个事件,进而触发淘宝的降价对应逻辑,然后由淘宝通知客户对应的商品降价了;
订阅和发布的顺序问题
普通情况下一般都是先订阅一个消息,然后再通过发布者进行发布消息进而收到推送消息;但是会有发布者在发布推送消息时,并没有订阅者进行接收,后续有订阅者后又不想错过订阅前的消息推送列表,此时就需要进行单独处理了;
实现思路
需要单独建立一个存放离线事件的
堆栈,当发布者进行事件推送时保存到存放离线事件的堆栈,其思路是按照时间节点为key进行相应的数组列表存储(需要设定对应的上限值,当超过上限后就添加新的字段表明还有更多的历史消息),等到有新的订阅者进行订阅时,就从缓存数据里进行相应读取执行推送的事件,离线事件也只执行一次,需要对用户订阅者进行是否读取缓存的字段标识(默认没有缓存数据时注册次字段为读取过);
发布订阅模式的相关应用
ES6 的Proxy应用
实现思路
在vue中可以通过Proxy和发布订阅模式进行响应式的实现,proxy在传入目标对象后就会检测目标对象上的属性追变化,当属性值变化时就会触发set API方法,此时就可以在set中进行响应式通知;
代码实现
(发布者)封装变化通知推送函数
// 通过Proxy监听对象变化
function eventEmitter(data, callback) {
return new Proxy(data, {
set(target, prop, value) {
data[prop] = value
callback()
return value
}
})
}
给目标对象(订阅者)添加检测功能
let Lbxin = {
name: "Lbxin",
age: 20
}
let LbxinInfo = Lbxin.name + Lbxin.age
Lbxin.age = 21;
console.log('检测前',LbxinInfo)
Lbxin = eventEmitter(Lbxin,()=>{LbxinInfo = Lbxin.name + Lbxin.age})
Lbxin.age = 21;
console.log('检测后',LbxinInfo)
// 检测前 Lbxin20
// 检测后 Lbxin21
会话过期刷token
当一个请求失败并返回了会话过期的状态码时,则需要判断是接口过期还是登录过期,如果是接口过期,则去请求新token,然后拿新token再次发起请求;
实现思路
- 在axios中添加
response响应拦截,当返回会话过期的错误码时就去调用刷token的逻辑; - 在刷token的逻辑里,需要先判断是否是登录过期(拿登录后保存的有效期与该接口的当前请求时间对比,当当前 时间减去接口请求时间小于有效期时表明不是登录过期,走刷token逻辑,否则走登录逻辑);
- 当为刷token逻辑时,为了避免多次请求获取token的接口需要添加一个标识,用于避免多次请求token接口;
- 在刷token的过程中,需要将新请求收集到订阅列表中,在token请求成功(发布者)后通知调度中心进行通知订阅者重新请求缓存的接口;
服务端处理方式
- 直接在服务端进行逻辑判断,当会话过期时,可以在
ResponseHeader里添加标识,表明会话是否已经过期; - 前端在axios中添加
response响应拦截,当发现有约定的会话过期的字段时就进行刷token或者跳转登录页面的操作;
中央事件总线EventEmitter的实现
实现思路及对应代码
- 创建一个
EventEmitter类,充当调度中心,在该类上创建对应的事件存储中心;
class EventEmitter {
constructor() {
//初始化事件列表
this.cache = {}
}
}
- 将订阅者注册到事件中心
//订阅事件
subscribe(name, fn) {
if (this.cache[name]) {
//可以采用对象的方式进行存储 删除的时候比数组的方式性能好一些
this.cache[name].push(fn)
} else {
//初始化事件
this.cache[name] = [fn]
}
//在订阅事件的时候可以返回唯一id的unsubscript事件进行定回调的取消订阅,代替简单通过函数对比进行卸载,订阅者在订阅的时候可以进行定向的接收,从而实现定向的回调卸载;
}
- 发布者的发布事件注册到调度中心,调度中心处理对应的发布逻辑
//发布事件
publish(name, once = false, ...args) {
if (this.cache[name]) {
//取出当前事件的所有回调函数 避免回调函数中再次注册回调函数造成死循环
let copyCache = this.cache[name].slice();
for (let fn of copyCache) {
//发布时进行相关的传参
fn(...args)
}
if (once) {
//当是一次性函数时进行销毁事件
delete this.cache[name]
}
} else {
return console.error(name + "not found!")
}
}
- 取消订阅事件
//卸载事件
unSubscribe(name, fn) {
let target = this.cache[name]
if (!fn) {
delete this.cache[name];
return consolr.log(name + '事件已全部卸载!')
}
if (target) {
const index = target.findIndex(f => f === fn || f.callback === fn)
if (index > -1) {
target.splice(index, 1)
}
}
}
- 只订阅一次,调用完毕后删除在事件存储中心存储的事件
// ......
if (once) {
//当是一次性函数时进行销毁事件
delete this.cache[name]
}
完整代码
class EventEmitter {
constructor() {
//初始化事件列表
this.cache = {}
}
//订阅事件
subscribe(name, fn) {
if (this.cache[name]) {
//可以采用对象的方式进行存储 删除的时候比数组的方式性能好一些
this.cache[name].push(fn)
} else {
//初始化事件
this.cache[name] = [fn]
}
//在订阅事件的时候可以返回唯一id的unsubscript事件进行定回调的取消订阅,代替简单通过函数对比进行卸载,订阅者在订阅的时候可以进行定向的接收,从而实现定向的回调卸载;
}
//卸载事件
unSubscribe(name, fn) {
let target = this.cache[name]
if (!fn) {
delete this.cache[name];
return consolr.log(name + '事件已全部卸载!')
}
if (target) {
const index = target.findIndex(f => f === fn || f.callback === fn)
if (index > -1) {
target.splice(index, 1)
}
}
}
//发布事件
publish(name, once = false, ...args) {
if (this.cache[name]) {
//取出当前事件的所有回调函数 避免回调函数中再次注册回调函数造成死循环
let copyCache = this.cache[name].slice();
for (let fn of copyCache) {
//发布时进行相关的传参
fn(...args)
}
if (once) {
//当是一次性函数时进行销毁事件
delete this.cache[name]
}
} else {
return console.error(name + "not found!")
}
}
}
案例分析
订阅报刊
按照发布订阅者模式,将该需求拆分成
订阅者、发布者和调度中心三部分:
订阅者只关注暴露报刊的订阅事件和发布后的推送接收事件,内部通过调度中心进行订阅报刊、推送接收与对应数据维护;
发布者只关注报刊类型的注册和报刊更新的推送事件,内部还是通过调度中心进行相关处理;
调度中心负责检测发布者的注册(维护报刊类数据)和推送事件(通知订阅者触发更新逻辑),订阅者的订阅和取消订阅报刊的事件和通知订阅者更新的逻辑;
// 报社
class Publisher {
constructor(name, channel) {
this.name = name;
this.channel = channel;
}
// 注册报纸 - 内部通过 调度中心 进行注册
addTopic(topicName) {
this.channel.addTopic(topicName);
}
// 推送报纸 - 内部通过 调度中心 进行推送
publish(topicName) {
this.channel.publish(topicName);
}
}
// 订阅者
class Subscriber {
constructor(name, channel) {
this.name = name;
this.channel = channel;
}
//订阅报纸 - 内部通过 调度中心 进行订阅
subscribe(topicName) {
this.channel.subscribeTopic(topicName, this);
}
//取消订阅 - 内部通过 调度中心 进行取消订阅
unSubscribe(topicName) {
this.channel.unSubscribeTopic(topicName, this);
}
//接收推送 - 内部通过 调度中心 进行订阅更新的接收
update(topic) {
console.log(`${topic}已经送到${this.name}家了`);
}
}
// 事件调度中心
// 注册报社 - 添加订阅者 - 检测发布者对应事件 - 根据发布者触发的事件通知订阅者
class Channel {
constructor() {
this.topics = {};
}
//报社在平台注册报纸
addTopic(topicName) {
this.topics[topicName] = [];
}
//报社取消注册
removeTopic(topicName) {
delete this.topics[topicName];
}
//订阅者订阅报纸
subscribeTopic(topicName, sub) {
if (this.topics[topicName]) {
this.topics[topicName].push(sub);
}
}
//订阅者取消订阅
unSubscribeTopic(topicName, sub) {
this.topics[topicName].forEach((item, index) => {
if (item === sub) {
this.topics[topicName].splice(index, 1);
}
});
}
//平台通知某个报纸下所有订阅者
publish(topicName) {
this.topics[topicName].forEach((item) => {
item.update(topicName);
});
}
}
const channel = new Channel();
// 报社只关注报社的注册逻辑(发布者),不需要关注订阅者的逻辑,也不需要维护注册后的列表维护
const pub1 = new Publisher("报社1", channel);
const pub2 = new Publisher("报社2", channel);
// 注册后通过调度中心进行注册报社
pub1.addTopic("晨报1");
pub1.addTopic("晚报1");
pub2.addTopic("晨报2");
订阅者同样不用关注报社的相关逻辑,只需要注册订阅者即可
const sub1 = new Subscriber("小明", channel);
const sub2 = new Subscriber("小红", channel);
const sub3 = new Subscriber("小张", channel);
// 注册后通过调度中心进行订阅报纸
sub1.subscribe("晨报1");
sub2.subscribe("晨报1");
sub2.subscribe("晨报2");
sub3.subscribe("晚报1");
sub3.subscribe("晨报2");
sub3.unSubscribe("晨报2");
//一切后续动作都由发布者进行触发,内部通过调度中心去通知订阅者进行后续逻辑操作,本身不用关注其他逻辑
pub1.publish("晨报1");
pub1.publish("晚报1");
pub2.publish("晨报2");
//晨报1已经送到小明家了
//晨报1已经送到小红家了
//晚报1已经送到小张家了
//晨报2已经送到小红家了
优缺点分析
优点
- 解耦发布者和订阅者:发布者和订阅者不需要有任何联系,不需要知道对应的需求逻辑,只需要关注对应的在调度中心中得到事件即可;
- 广泛应用于异步编程中:在接口的异步请求中,不知道接口的返回顺序,通常是加入回调进行处理,但在发布订阅模式中,异步逻辑(发布者)只需要在外部的调度中心订阅一个ready事件,在对应异步结束后去调用调度中心订阅的ready事件,当 满足条件后再由调度中心去通知订阅者处理对应逻辑;
缺点
- 因为将发布者与订阅者进行解耦,两者无法通过简单的方式进行通信,当需要进行通信时就需要通过事件的方式进行通信了;
- 发布订阅者模式弱化了对象间的联系,当使用不当,对象间的关系将难以追踪和维护;
- 使用发布订阅者模式时会应用到数据存储和消息推送等逻辑,会造成一定的时间和内存的消耗;