观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,全部依赖于它的对象都将获得通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通信,观察者模式就是观察者和被观察者之间的通信。node
观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一块儿的,当订阅目标发生改变时,逐个通知订阅者。编程
其实24种基本的设计模式中并无发布订阅模式,上面也说了,他只是观察者模式的一个别称。
可是通过时间的沉淀,彷佛他已经强大了起来,已经独立于观察者模式,成为另一种不一样的设计模式。
在如今的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤全部发布者传入的消息并相应地分发它们给订阅者。设计模式
背景:成都老妈兔头真香,买的人太多须要预约才能买到,因此顾客就等于了订阅者,订阅老妈兔头。
而老妈兔头有货了得通知顾客来买啊,否则没有钱赚,得通知全部的订阅者有货了来提兔头,这时老妈兔头这家店就是发布者。缓存
/*兔头店*/ var shop={ listenList:[],//缓存列表 addlisten:function(fn){//增长订阅者 this.listenList.push(fn); }, trigger:function(){//发布消息 for(var i=0,fn;fn=this.listenList[i++];){ fn.apply(this,arguments); } } } /*小明订阅了商店*/ shop.addlisten(function(taste){ console.log("通知小明,"+taste+"味道的好了"); }); /*小龙订阅了商店*/ shop.addlisten(function(taste){ console.log("通知小龙,"+taste+"味道的好了"); }); /*小红订阅了商店*/ shop.addlisten(function(taste){ console.log("通知小红,"+taste+"味道的好了"); }); // 发布订阅 shop.trigger("中辣");
//console 通知小明,中辣味道的好了 通知小龙,中辣味道的好了 通知小红,中辣味道的好了
上面的案例存在问题,由于在触发的时候是将因此的订阅都触发了,并无区分和判断,因此须要一个Key来区分订阅的类型,而且根据不一样的状况触发。并且订阅是能够取消的。服务器
升级思路:架构
/*兔头店*/ var shop={ listenList:{},//缓存对象 addlisten:function(key,fn){ // 没有没有key给个初值避免调用报错 if (!this.listenList[key]) { this.listenList[key] = []; } // 增长订阅者,一个key就是一种订阅类型 this.listenList[key].push(fn); }, trigger:function(){ const key = Array.from(arguments).shift() const fns = this.listenList[key] // 这里排除两种特殊状况,第一种为触发的一种从未订阅的类型,第二种订阅后取消了全部订阅的 if(!fns || fns.length===0){ return false; } // 发布消息,触发同类型的全部订阅, fns.forEach((fn)=>{ fn.apply(this,arguments); }) /* for(var i=0,fn;fn=fns[i++];){ fn.apply(this,arguments); } */ }, remove:function(key,fn){ var fns=this.listenList[key];//取出该类型的对应的消息集合 if(!fns){//若是对应的key没有订阅直接返回 return false; } if(!fn){//若是没有传入具体的回掉,则表示须要取消全部订阅 fns && (fns.length=0); }else{ for(var l=fns.length-1;l>=0;l--){//遍历回掉函数列表 if(fn===fns[l]){ // 这里是传入地址的比较,因此不能直接用匿名函数了 fns.splice(l,1);//删除订阅者的回掉 } } } } } function xiaoming(taste){ console.log("通知小明,"+taste+"味道的好了"); } function xiaolong(taste){ console.log("通知小龙,"+taste+"味道的好了"); } function xiaohong(taste){ console.log("通知小红,"+taste+"味道的好了"); } // 小明订阅了商店 shop.addlisten('中辣',xiaoming); shop.addlisten('特辣',xiaoming); // 小龙订阅了商店 shop.addlisten('微辣',xiaolong); // 小红订阅了商店 shop.addlisten('中辣',xiaohong); // 小红忽然不想吃了 shop.remove("中辣",xiaohong); // 中辣口味作好后,发布订阅 shop.trigger("中辣"); shop.trigger("微辣"); shop.trigger("特辣");
咱们一般所看到的都是先订阅再发布,可是必需要遵照这种顺序吗?答案是不必定的。若是发布者先发布一条消息,可是此时尚未订阅者订阅此消息,咱们能够不让此消息消失于宇宙之中。就如同QQ离线消息同样,离线的消息被保存在服务器中,接收人下次登陆以后,才会收到此消息。一样的,咱们能够创建一个存放离线事件的堆栈,当事件发布的时候,若是此时尚未订阅者订阅这个事件,咱们暂时把发布事件的动做包裹在一个函数里,这些包装函数会被存入堆栈中,等到有对象来订阅事件的时候,咱们将遍历堆栈并依次执行这些包装函数,即重发里面的事件,不过离线事件的生命周期只有一次,就像qq未读消息只会提示你一次同样。app
发布-订阅的优点很明显,作到了时间上的解耦和对象之间的解耦,从架构上看,MVC,MVVM都少不了发布-订阅的参与。
一样的node中的EventEmitter也是发布订阅的异步