JavaScript设计模式_05_发布订阅模式
发布-订阅模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都将得到通知。发布-订阅模式是使用比较广泛的一种模式,尤其是在异步编程中。
/* * pre:发布-订阅模式 * 一种一对多的关系 */ // ------ 示例1 ---------- /** * 示例:售楼处售楼,购买者询问价格,售楼MM每天要接N多个电话,内容大致都相同,这很是麻烦。 * 于是售楼MM想到把他们的电话号码记在花名册上,每次一有楼盘的信息,就挨个给他们发消息。 * 我们将这个过程,抽象为代码如下: */ var salesOffice = {}; salesOffice.clientList = []; salesOffice.listen = function(fn) { this.clientList.push(fn); }; salesOffice.trigger = function() { for(var i = 0, fn; fn = this.clientList[i++];) { fn.apply(this, arguments); } }; salesOffice.listen(function(price, squareMeter) { // 订阅者1 console.log("price:" + price); console.log("squareMeter:" + squareMeter); }); salesOffice.listen(function(price, squareMeter) { // 订阅者2 console.log("定价:" + price); console.log("平方数:" + squareMeter); }); salesOffice.trigger(2000000, 70); // 发布 // ----- 示例2 -------- /* 示例1的不足:订阅者收到了发布者发布的每一条消息,但每个订阅者关注的可能不一样, * 比如小明关注80平米左右的房子,小王关注100平米以上的房子。 * 接下来我们修改程序,示例代码如下: */ var salesOffice = {}; salesOffice.clientList = {}; // 使用对象字面量,进行缓存 salesOffice.listen = function(key, fn) { if(!this.clientList[key]) { this.clientList[key] = []; } this.clientList[key].push(fn); }; salesOffice.trigger = function() { var key = Array.prototype.shift.apply(arguments); if(this.clientList.size == 0 || !this.clientList[key]) { console.log("订阅者为空."); return; } for(var fn of this.clientList[key]) { fn.apply(this, arguments); } }; salesOffice.listen(8, function(price, squareMeter) { console.log("类型:" + 8 + ",price:" + price + ",squareMeter:" + squareMeter); }); salesOffice.listen(10, function(price, squareMeter) { console.log("类型:" + 10 + ",price:" + price + ",squareMeter:" + squareMeter); }); salesOffice.trigger(8, 2000000, 88); //----------- 示例3 ------------ /* 试想:如果其他售楼处,也想有发布-订阅功能,是否要将代码重写一次呢?完全没必要。 * 接下来,我们将上面的代码进行抽象,将客户缓存,以及监听和发布事件提取出来。 * 给需要的对象进行浅拷贝。 */ var event = { clientList: {}, listen: function(key, fn) { if(!this.clientList.hasOwnProperty(key)) { this.clientList[key] = []; } this.clientList[key].push(fn); }, trigger: function() { var key = Array.prototype.shift.call(arguments); if(this.clientList.size == 0 || !this.clientList[key]) { console.log("订阅者为空."); return; } for(var fn of this.clientList[key]) { fn.apply(this, arguments); } } }; var installEvent = function(obj) { for(var k in event) { obj[k] = event[k]; } }; var salesOffice = {}; installEvent(salesOffice); salesOffice.listen(9, function(price, squareMeter) { console.log("key:9" + ",price:" + price + ",squareMeter:" + squareMeter); }); salesOffice.listen(10, function(price, squareMeter) { console.log("key:10" + ",price:" + price + ",squareMeter:" + squareMeter); }); salesOffice.trigger(9, 3000000, 91); //----------- 示例4 ------------- /* 增加 - 删除订阅功能 * 如果只传订阅类型,则删除这一组订阅者。 * 如果传了订阅类型,以及订阅者,则删除该订阅者 */ var event = { clientList: {}, listen: function(key, fn) { if(!this.clientList.hasOwnProperty(key)) { this.clientList[key] = []; } this.clientList[key].push(fn); }, trigger: function() { var key = Array.prototype.shift.call(arguments); if(this.clientList.size == 0 || !this.clientList[key]) { console.log("订阅者为空."); return; } for(var fn of this.clientList[key]) { fn.apply(this, arguments); } } }; event.remove = function(key, fn) { var fns = this.clientList[key]; if(!fns) { return false; } if(!fn) { fns.length = 0; } else { for(var k in fns) { if(fns[k] === fn) { fns.splice(k, 1); return; } } } }; var installEvent = function(obj) { for(var k in event) { obj[k] = event[k]; } }; var salesOffice = {}; installEvent(salesOffice); salesOffice.listen(9, fn1 = function(price, squareMeter) { console.log("fn1 - key:9" + ",price:" + price + ",squareMeter:" + squareMeter); }); salesOffice.listen(9, fn2 = function(price, squareMeter) { console.log("fn2 - key:9" + ",price:" + price + ",squareMeter:" + squareMeter); }); salesOffice.remove(9, fn2); salesOffice.trigger(9, 2000000, 90); // -------------- 示例5 -------------- /* [全局发布-订阅] * 上面的示例中,如果有两个售楼处,我们就要创建两个对象。现实中,我们会有中介的存在, * 我们不要关心是哪个售楼处的楼盘,只要我们跟中介说,有xx平米的房子,就给我发消息。 * 于是,我们将上面的程序再一次抽象。 */ var Event = (function() { var clientList = {}; var listen = function(key, fn) { if(!clientList.hasOwnProperty(key)) { clientList[key] = []; } clientList[key].push(fn); }; var remove = function(key, fn) { var fns = clientList[key]; if(!fns) { return false; } if(!fn) { fns && (fn.length = 0); } else { for(var a in fns) { if(fns[a] === fn) { fns.splice(a, 1); return; } } } }; var trigger = function() { var key = Array.prototype.shift.call(arguments); var fns = clientList[key]; if(!fns) { return false; } for(var fn of fns) { fn.apply(this, arguments); } }; return { listen: listen, remove: remove, trigger: trigger } })(); Event.listen(8, function(price, squareMeter) { console.log("key:8," + "price:" + price + ",squareMeter:" + squareMeter); }); Event.trigger(8, 3000000, 83); //------------ 示例6 ------------- /* * [先发布-后订阅] * 在现实中,我们往往需要先发布,后订阅,订阅后消息发送一次 * 接下来修改程序如下: * */ var Event = (function() { var clientList = {}, cache = {}, listen, remove, trigger; listen = function(key, fn) { if(!clientList.hasOwnProperty(key)) { clientList[key] = []; } clientList[key].push(fn); // 判断是否有未消费的消息 if(!cache.size != 0) { for(var a in cache) { var arr = a.split(","); if(arr[0] != key) { continue; } arr.splice(0, 1);// 移除key,只保留价格和平方数 var fns = cache[a]; if(fns.length == 0) {// 没有消费者 fn.apply(this, arr); fns.push(fn); } else {// 消息消费者没有当前的订阅者 var flag = false; for(var t of fns) { if(t === fn) { flag = true; } } if(!flag) { fn.apply(this, arr); fns.push(fn); } } } } }; remove = function(key, fn) { var fns = clientList[key]; if(!fns) { return false; } if(!fn) { fns && (fn.length = 0); } else { for(var a in fns) { if(fns[a] === fn) { fns.splice(a, 1); return; } } } }; trigger = function() { var p = Array.prototype.join.call(arguments, ","); var key = Array.prototype.shift.call(arguments); var fns = clientList[key]; cache[p] = []; if(!fns) { // 没有订阅者 return false; } for(var fn of fns) { fn.apply(this, arguments); cache[p].push(fn); // 缓存消费的订阅者 } }; return { listen: listen, remove: remove, trigger: trigger } })(); Event.trigger(8, 3000000, 83); Event.listen(8, fn1 = function(price, squareMeter) { console.log("key:8," + "price:" + price + ",squareMeter:" + squareMeter); }); // ----------- 示例7 ------------ /* [应用] * 在web系统中,当用户登录成功后,我们需要做很多事情,比如在顶部加载头像,刷新地址等。 * 使用发布-订阅模式,可以帮我们更好的去实现这一功能。 * 示例如下: */ var login = {}; installEvent(login); var header = (function() { login.listen("loginSuc", function(data) { header.setAvatar(data.avatar); }); return { setAvatar: function(avatar) { console.log("设置头像:" + avatar); } } })(); var address = (function() { login.listen("loginSuc", function() { address.refresh(); }); return { refresh: function() { console.log("刷新地址."); } } })(); login.trigger("loginSuc", { avatar: "xxx" }); //---------- 示例7 ------------ /* [应用] * 模块间通信 * 示例:页面上有一个按钮,和一个div,我们每点击一次按钮, * div里就显示我们点击的次数,我们使用发布-订阅模式实现 * <button id="btn">点我</button> * <div id="show"></div> */ var a = (function() { var div = document.getElementById("show"); Event.listen("add", function(count) { div.innerHTML = count; }); })(); var b = (function() { var count = 0; var btn = document.getElementById("btn"); btn.onclick = function() { Event.trigger("add", ++count); } })(); //=============== 总结 ================= /** * 通过以上的示例,我们可以看到发布-订阅的模式,优点十分明显。 * 优点:1、时间上的解耦;2、对象之间的解耦。非常适用于异步编程,以及对象之间松耦合的实现。 * 但发布-订阅模式也有很多缺点。 * 缺点:1、创建订阅者消耗一定的内存和时间,当你订阅一个消息后,可能这个消息至始至终都没有发生, * 但订阅者一直存在内存中。 * 2、发布-订阅模式弱化了对象之间的联系,如果过度使用的话,对象之间的必要联系就会被深埋在背后。 * 特别是嵌套使用的时候,理解起来就比较费时。 */
作者:『Stinchan』
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。