JS常见设计模式 之 发布—订阅
对于我们前端仔而言,最最最耳熟能详的应该就是发布订阅模式,毕竟我们前端三大框架之一Vue的核心设计模式之一就是设计模式。
发布——订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知
现实中的发布——订阅模式
不论实在程序世界里还是现实生活中,发布——订阅模式的应用都非常广泛。
比如我最近想买一个二手的键盘,于是我去某鱼上面逛,发现有一个卖家卖的键盘和价格都比较满意,于是我兴高采烈的去发消息给卖家询问买卖事宜。可是很不幸的是卖家似乎不在线,于是我就只能在卖家那儿留下我的联系方式,让卖家有时间的时候就来联系我。后面卖家上线了,就来联系我了。
上面的这个例子里我是订阅者,卖家是发布者,我在卖家那里留下我的联系方式订阅他的消息,同时也有可能其他的买家和我一样在他那里留下了联系方式,然后等到买家上线的时候,他就给每个订阅者都发消息。这就是一个生活中很常见的例子。
发布订阅的作用
在刚刚的例子中的发布——订阅模式有着显而易见的特点:
- 买家不需要时时刻刻的盯着手机等待着卖家上线发消息,只需要卖家有时间了再来通过更好的联系方式来联系买家。
- 如果卖家后续还有键盘要卖出同时我也是一个键盘发烧友的话,卖家可以更加直接的联系我。
而在软件开发中,发布订阅模式也是如此,最最最常见之一的就是前端对后端发起网络请求,当我们使用ajax或者axios的时候,我们通常都会在回调函数中写上我们需要做的一系列操作,其实这个就相当于我们去订阅了这个网络请求,我们并不知道什么时候请求会结束,但是没有关系,我们只需要订阅了这个请求,请求结束之后就会执行我们所订阅的操作。
同样的,在对象与对象之间我们也可以用发布订阅机制实现对象与对象之间的解耦。将对象分离成发布者和订阅者对象,每次当有新的订阅者对象出现的时候我们不需要去修改发布者对象的相关逻辑代码,只需要让新的订阅者对象去订阅发布者即可,使得代码更加的松耦合多变。
DOM事件
实际上我们只要曾经在DOM节点上面绑定过事件函数,那我们就曾经用过发布——订阅模式:
document.body.addEventListener('click', function() {
alert('我被点击了')
}, false)
document.body.click() // 模拟用户点击
我们开发者是没法知道用户什么时候回去点击,所以我们订阅了document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。
同时我们还可以随意的增加新的订阅者,任何新增加的订阅者都不会影响到发布者:
document.body.addEventListener('click', function() {
alert('我被点击了')
}, false)
document.body.addEventListener('click', function() {
alert('body被我点击了')
}, false)
document.body.addEventListener('click', function() {
alert('错,是我点击了body')
}, false)
document.body.click() // 模拟用户点击
自定义发布订阅事件
有一天小明因为受伤住院了,身为在外乡苦逼漂泊的打工人并没有人来照顾他,小明迫不得已只能点医院的午餐吃,让我们来实现一下这个过程吧:
// 需要订餐的住院病人
const Patient = {
name: 'xiaoming'
}
// 中午送餐人员
const Staff = {
OrderedList: [],
deliver: function () {
this.OrderedList.forEach(personName => personName && console.log(`${personName},你的饭到了!`))
},
subscribe: function (Patient) {
this.OrderedList.push(Patient.name)
}
}
Staff.subscribe(Patient)
Staff.deliver()
这样,我们就完成了一个简单的发布——订阅模式
取消订阅事件
正当小明心情郁闷的吃着医院餐的时候,小明的母亲知道小明住院了来到他所在城市,天天做饭给小明吃(世上只有妈妈好~)。为了避免浪费粮食,小明就需要取消之前的订阅:
// 需要订餐的住院病人
const Patient = {
name: 'xiaoming'
}
// 中午送餐人员
const Staff = {
OrderedList: [],
deliver: function () {
this.OrderedList.forEach(personName => personName && console.log(`${personName},你的饭到了!`))
},
subscribe: function (Patient) {
this.OrderedList.push(Patient.name)
},
cancel: function ({ name }) {
this.OrderedList.splice(this.OrderedList.indexOf(Patient.name), 1)
}
}
Staff.subscribe(Patient)
Staff.deliver()
Staff.cancel(Patient)
Staff.deliver()
真实例子——网站登录
相信前端大伙们绝大多数都开发过后台管理系统,说到后台管理系统那就离不开离不开登录这个操作,而且我们在登录之后都要根据登录信息去做一系列操作,比如用户token存起来,又比如去根据登录信息生成新的动态路由。
从上面的例子可以看出来,存用户信息和根据登录信息生产新的动态路由其实是两个不相干的事情,但是很多不成熟的开发人员都会把这些操作全部写在一个回调方法里,可能两个看着还好,但是当需要处理的事情多了之后,代码就会显得很臃肿不堪难以维护!
所以我们需要对这部分代码进行解耦把整个登录拆分成发布者,将存用户信息和根据信息生成新的动态路由拆分成两个不相干的订阅者,让这两个订阅者都去订阅登录事件:
const loginApi = () => new Promise(resolve => {
// 暂定一秒后登录成功
setTimeout(() => {
resolve({
userInfo: '我是userInfo',
routerTree: ['我是routerTree']
})
}, 1000)
})
// 登录发布者
const loginPublisher = {
handleList: [],
login: function () {
loginApi().then(res => this.handleList.forEach(fn => fn(res)))
},
subscribe: function (fn) {
this.handleList.push(fn)
}
}
// 存userInfo
const setUserInfo = ({ userInfo }) => localStorage.setItem('userInfo', userInfo)
// 生产新的路由表
const getNewRouterMap = ({ routerTree }) => console.log(routerTree)
// 订阅
loginPublisher.subscribe(setUserInfo)
loginPublisher.subscribe(getNewRouterMap)
// 登录操作
loginPublisher.login()
必须先订阅再发布吗
我们目前所知道的发布订阅模式,都是订阅者先订阅一个消息,然后才能接受到发布者发布的消息。如果我们把顺序反过来,先发布一个消息,这个时候如果没有订阅者订阅这个消息,那么这个消息是不是就没有用了。
然而在一些情况下,当发布者消息发布的时候并没有订阅者订阅,等到订阅者再来订阅的时候就会错失了之前的那个消息。
所以我们遇到这种情况的时候该怎么做呢,我们需要定义一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅事件,我们暂时把发布时事件的动作包裹再一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。
总结
发布订阅模式有很明显的优点,一为时间上的解耦,二为对象之间的解耦。它可以帮助我们完成更加松耦合的代码编写
当然发布订阅模式的也是有缺点的,首先如果只有一个订阅者和一个发布者的情况下,我们是没必要去用发布订阅者模式去进行解耦的,因为这样反而会增加代码量和打码效率。同时发布订阅模式虽然可以让对象之间关系弱化形成松耦合,但是如果过度使用或者使用过于分散的话,对象与对象之间的关系将会难以维护,导致代码后续维护和理解。
转载自:https://juejin.cn/post/7148358302873681933