不论是在程序世界里还是现实生活中,发布—订阅模式的应用都非常广泛。我们先看一下现实中的例子。
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼MM告诉小明,不久后还有一些尾盘推出。开发商正在办理相关手续,手续办好后便可以购买。但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除了小明,还有小红,小强,小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼MM决定辞职,因为厌倦了每天回答1000个相同的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在售楼处。售楼MM答应他,新楼盘一推出就马上发信息通知小明。小红、小强、小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼MM会翻开花名册,遍历上面的电话号码,依次发送一条短信通知他们。
发布订阅模式的优点
-
-
可以广泛应用于异步编程,它可以代替我们传统的回调函数,我们不需要关注对象在异步执行阶段的内部状态,我们只关心事件完成的时间点。
-
取代对象之间硬编码通知机制,一个对象不必显式调用另一个对象的接口,而是松耦合的联系在一起
虽然不知道彼此的细节,但不影响相互通信。更重要的是,其中一个对象改变不会影响另一个对象。
-
我们实现一个简单地发布订阅模型:
1 // 首先定义消息的发布者 2 const salesoffice = {}; 3 4 // 定义缓存列表,存放订阅者的回调函数列表; 5 salesoffice.clientList =[]; 6 7 // 设置订阅者 8 9 salesoffice.listen = function(key, fn){10 if(!this.clientList[key]) {11 this.clientList[key] = [];12 }13 this.clientList[key].push(fn);14 }15 16 // 设置发布事件17 salesoffice.trigger = function(){18 var key = Array.prototype.shift.call(arguments),19 fns = this.clientList[key];20 if(!fns || fns.length === 0) {21 return false;22 }23 for(var i = 0 , fn ; fn = fns[i++];) {24 fn.apply(this, arguments);25 }26 }27 28 //示例29 30 salesoffice.listen('aaaaa' , function(a, b){31 console.log(a);32 console.log( b);33 })34 35 36 salesoffice.listen('abcde' , function(a, b){37 console.log(a);38 console.log(b);39 console.log(a+b);40 }) 41 42 console.log(salesoffice);43 44 salesoffice.trigger('aaaaa' , 100 , 200); // 100 200 30045 46 salesoffice.trigger('abcde', 111 , 222); // 111 222 333
一个通用的发布订阅模型和事件
1 // 定义发布/订阅模型 2 const event = { 3 // 设置缓存列表,存放订阅者的回调函数列表 4 list : [], 5 // 设置订阅者 6 listen : function(key , fn){ 7 if(!this.list[key]){ 8 this.list[key] = []; 9 }10 // 将订阅的消息添加到缓存列表中11 this.list[key].push(fn);12 },13 14 // 发布事件15 trigger : function() {16 const key = Array.prototype.shift.call(arguments),17 fns = this.list[key];18 if(!fns || fns.length === 0){19 return false;20 }21 for(let i = 0 , fn ; fn = fns[i++];){22 fn.apply(this, arguments);23 }24 },25 26 // 取消订阅27 remove : function(key , fn){28 const fns = this.list[key];29 // 如果key对应的消息没有订阅过的话,则返回30 if(!fns) {31 return false;32 }33 // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅34 if(!fn) {35 delete this.list[key]; //如果没有后续参数,则删除整个回调数组36 }else {37 for(let i = fns.length - 1 ; i>=0 ;i--) {38 const _fn = fns[i];39 if(_fn === fn) {40 fns.splice( i, 1); // 删除订阅者的回调函数41 }42 } 43 }44 }45 };46 47 const initEvent = function(obj) {48 for(let i in event) {49 obj[i] = event[i];50 }51 };52 53 const shoeobj = {};54 55 initEvent(shoeobj);56 57 shoeobj.listen('abcd' , function(a, b) {58 console.log(a);59 console.log(b);60 })61 62 shoeobj.listen('efgh' , function(a, b){63 console.log(a);64 console.log(b);65 })66 67 shoeobj.trigger('abcd' , 100 ,200); // 100 20068 69 shoeobj.trigger('efgh' , 300, 500); // 300 50070 71 shoeobj.remove('abcd');72 73 shoeobj.trigger('abcd', 20, 50); // false