发布订阅模式(观察者模式)
定义:定义对象之间一对多的依赖关系,当一个对象状态发生改变时,所有依赖它的对象都将得到通知
发布-订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要做任何改变,同样发布者需求改变时,也不会影响到之前的订阅者。只要约定的事件名没有变化,就可以自由地改变它们
实现发布-订阅模式
- 指定发布者
- 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
- 最后发布消息的时候,发布者遍历缓存列表,依次触发里面存放的订阅者回调函数
- 取消订阅:删掉缓存列表中对应的回调函数
通用的发布-订阅模式
先把发布-订阅的功能提取出来,放在一个单独的对象内
const event={
clientList:{},
listen:function(key,fn){
if(!this.clientList[key]){
this.clientList[key]=[];
}
this.clientList[key].push(fn);//订阅的消息加入缓存列表
},
trigger:function(){
const key=Array.prototype.shift.call(arguments);
const fns=this.clientList[key];
//如果没有绑定对应的消息
if(!fns||fns.length===0) return false;
for(let i=0,fn;fn=fns[i++];){
fn.apply(this,arguments);//arguments是调用trigger时所带上的参数
}
}
}
再定义一个installEvent函数,这个函数可以给所有对象都动态安装发布-订阅功能
const installEvent=function(obj){
for(let key in event){
obj[key]=event[key];
}
}
测试——给售楼处对象salesOffice动态添加发布-订阅功能
const salesOffices={};
installEvent(salesOffices);
salesOffices.listen('square88',function(price){//小明订阅消息
console.log(`价格:${price}`);
});
salesOffices.listen('square110',function(price){//小红订阅消息
console.log(`价格:${price}`);
});
salesOffices.trigger('square88',880000);//价格:880000
salesOffices.trigger('square110',1100000);//价格:1100000
取消订阅的事件
event.remove=function(key,fn){//取消订阅
const fns=this.clientList[key];
//如果对应的key值没人订阅,直接返回
if(!fns) return false;
//如果没有传入回调函数,表示要取消key值对应消息的所有订阅
if(!fn){
fns&&(fns.length=0);
}else{
//反向遍历订阅的回调函数列表
for(let i=fns.length-1;i>=0;i--){
if(fns[i]===fn){
//删除订阅者的回调函数
fns.splice(i,1);
}
}
}
}
测试:
const fn=function(price){
console.log(`价格:${price}`);
}
salesOffices.listen('square88',fn);//订阅
salesOffices.remove('square88');//取消订阅
全局的发布-订阅对象
发布-订阅模式中,可以用一个全局的Event对象来实现,订阅者不需要了解信息来自哪个发布者,发布者也不知道信息会推送给哪个订阅者,Event作为一个类似“中介”的角色,把发布者和订阅者联系起来
//全局的发布-订阅对象
const Event=(function(){
const clientList={};
//订阅消息
const listen=function(...args){
const key=args[0];
const fn=args[1];
if(!clientList[key]){
clientList[key]=[];
}
clientList[key].push(fn);//订阅的消息加入缓存列表
}
//发布消息
const trigger=function(){
const key=Array.prototype.shift.call(arguments);
const fns=clientList[key];
//如果没有绑定对应的消息
if(!fns||fns.length===0) return 0;
for(let i=0,fn;fn=fns[i++];){
fn.apply(this,arguments);//arguments是调用trigger传入的参数
}
}
//取消订阅
const remove=function(...args){
const key=args[0],fn=args[1];
const fns=clientList[key];
//未订阅,直接返回
if(!fns||fns.length===0) return false;
//未传入fn,取消key所有相关的订阅
if(!fn){
fns&&(fns.length=0);
}else{
//反向遍历缓存列表
for(let i=fns.length-1;i>=0;i--){
if(fns[i]===fn){
//从缓存列表删除订阅者的回调函数
fns.splice(i,1);
}
}
}
}
return {
listen,
trigger,
remove
}
})();
const fn=function(price){
console.log(`价格:${price}`);
}
Event.listen('square90',fn);
Event.listen('square10',fn);
Event.remove('square10',fn);
Event.trigger('square90',1000);
Event.trigger('square10',3000);
//输出:价格:1000
模块间通信
点击a模块里的按钮,b模块显示按钮点击次数:
const a=(function(){
let count=0;
const btn=document.querySelector('#btn');
btn.addEventListener('click',function(){
Event.trigger('add',++count);
});
})();
const b=(function(){
const divObj=document.querySelector('#text');
Event.listen('add',function(count){
divObj.innerHTML=count;
})
})();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfrE5Twv-1637981929666)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20211113113537094.png)]
先发布再订阅
某些情况下,我们需要先将消息保存下来,等到有对象订阅它的时候,再重新安发布给订阅者。就像QQ的离线消息一样,离线消息先被保存在服务器中,接收人下次登录的时候,可以重新接收到这条消息
为了满足这类需求,可以建立一个存放离线事件的堆栈,事件发布的时候,还没有订阅者来订阅这个事件,就暂时先把发布事件的动作包裹在一个函数里,这些包装函数将会被保存在堆栈中,等到有订阅者订阅消息的时候,再遍历堆栈并且依次执行这些包装函数,也就是重新发布这些事件(离线事件生命周期只有一次,就像qq消息只会被重新阅读一次)
总结
发布-订阅模式,又称观察者模式,在实际开发中非常有用
优点:
- 时间上的解耦
- 对象之间的解耦
缺点:
-
创建订阅者本身需要消耗一定的时间和内存,而当你订阅一个消息后,也许消息最后都没有发生,但这个订阅者会始终存在于内存中
-
可以弱化对象之间的联系,如果过度使用的话,对象之间的必要联系会被深埋在背后,会导致程序难以跟踪维护和理解,特别是多个发布者和订阅者嵌套在一起的时候
发布-订阅模式的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写,还可以用来帮助实现一些别的设计模式,比如中介者模式