JavaScript发布订阅模式学习
1、什么是发布订阅模式?
发布订阅:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。
拿公众号举例:
- 只有该公众号的订阅者才能收到推送
- 公众号只负责推送信息,不关心是谁订阅了我,只要有信息推送,那么就推送给所有的订阅者
- 订阅者无需时不时的查看公众号是否有信息推送,只要公众号推送信息后,该订阅者就会收到通知
- 订阅者可随时取消对该公众号的订阅
在调用方法时首先要发布方法,确保调用方法能够正常调用到。可以向一类相同事件中添加很多方法。当调用这一类方法时,可以统一调用整个流程。
发布订阅者模式,可以让我们不再涉及更多的回调处理,而且可以使模块的颗粒度更小。比如有个ajax的数据展示,其中一个订阅者A可以只负责数据的表格展示,另一个订阅者B只负责数据总量的计算。当有需求要把数据总量的计算修改为当前页的数据总量和整体的数据总量计算,那么订阅者A是不用任何变动的。
2、发布订阅是为了解决什么问题?
发布订阅者模式是为了发布者和订阅者之间避免产生依赖关系,发布订阅者之间的订阅关系由一个中介列表来维护。发布者只需做好发布功能,至于订阅者是谁,订阅者做了什么事情,发布者是无需关心的
3、发布订阅的优缺点
发布订阅模式确实为我们的代码带来最小的耦合,并不是所有场景都适合使用这种模式,这种模式也有其利弊。
优点:
- 支持简单的广播通信,自动通知所有已经订阅过的对象。
- 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
- 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。
缺点:
模块之间如果用了太多的全局发布-订阅模式来通信,那么模块与模块之间的联系就被隐藏到了背后,我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给其他模块调用。
4、简单实现发布订阅模式
/** * 创建发布订阅模式(观察者模式) */ const PublishSubscribeMode= (function () { // 创建缓存事件对象,将订阅的事件列表存入 const _cacheEvent= {}; /** * 订阅方法 * @param key 订阅的事件名(属性名) * @param callback 订阅的回调函数(用于接收通知) */ const subscribe = function (key, callback) { if(!_cacheEvent[key]) { _cacheEvent[key] = []; // 初始化事件订阅列表 } // 将订阅的回调存入事件列表 // 不关心有多少订阅,只要有进行订阅则将回调存入列表 _cacheEvent[key].push(callback); } /** * 发布方法 */ const publish = function () { // 获取订阅的事件key(删掉了arguments第一项) let key = Array.prototype.shift.call(arguments); // 获取订阅的事件列表 let subscribeEvents = _cacheEvent[key] || []; // 没有订阅直接结束 if(subscribeEvents.length == 0) { return; } // 遍历订阅事件列表并依次调用,将发布的数据传入回调函数 for (let i = 0, fn; fn = subscribeEvents[i++];) { fn.apply(null, arguments); } } /** * 取消订阅 * @param key 订阅的事件名(属性名) * @param fn 订阅的回调函数(订阅时传入的接收通知的回调函数) * @return {boolean} 取消订阅成功返回true,否则false */ const removeSubscribe = function (key, fn) { // 获取订阅的事件列表 let subscribeEvents = _cacheEvent[key] || []; // 如果订阅不存在,则返回false if(subscribeEvents.length == 0) { return false; } // 遍历订阅的事件列表 // 找到对应的订阅回调并从事件列表中移除 for (let i = 0; i < subscribeEvents.length; i++) { if(fn == subscribeEvents[i]) { subscribeEvents.splice(i, 1); } } return true; } // 返回对象功能 return { subscribe, publish, removeSubscribe }; })() /** * 使目标对象具有发布订阅的功能 * @param target 目标对象 */ const initPublishSubscribeMode = function (target) { // 非对象类型过滤 if(Object.prototype.toString.call(target) != '[object Object]') { throw Error('target is not Object'); } // 遍历源对象,使目标对象继承源对象的功能 for (let key in PublishSubscribeMode) { target[key] = PublishSubscribeMode[key]; } return target; }
发布订阅实现双向绑定使用案例:
<button id="btn">走你</button> <div id="text"></div>
// 为其他对象添加发布订阅功能 var obj = {}; initPublishSubscribeMode(obj); let btn = document.getElementById('btn'); let text = document.getElementById('text'); let count = 0; // 订阅add事件 obj.subscribe('add', function (count) { text.innerText = count; // 改变div的内容 }) btn.onclick = function () { obj.publish('add', count++); // 点击按钮发布add事件,使count++ }
class重构版
class PublishSubscribeMode { constructor() { // 创建缓存事件对象,将订阅的事件列表存入 this.cacheEvent = {}; } /** * 订阅方法 * @param key 订阅的事件名(属性名) * @param callback 订阅的回调函数(用于接收通知) */ subscribe(key, callback) { if(!this.cacheEvent[key]) { this.cacheEvent[key] = []; } this.cacheEvent[key].push(callback); } /** * 发布方法 * @param arg arguments对象 */ publish(...arg) { // rest运算符(...)会将arguments转换为真正的数组,可以直接调用数组方法 let key = arg.shift(); let subscribeEvents = this.cacheEvent[key] || []; if(subscribeEvents.length === 0) { return; } subscribeEvents.forEach((callbackItem) => { callbackItem.call(null, ...arg) }) } /** * 取消订阅 * @param key 订阅的事件名(属性名) * @param fn 订阅的回调函数(订阅时传入的接收通知的回调函数) * @return {boolean} 取消订阅成功返回true,否则false */ removeSubscribe(key, fn) { let fns = this.cacheEvent[key] || []; if(fns.length == 0){ return false; } fns.forEach((item, index) => { if(fn == item) { fns.splice(index, 1); } }) return true; } }
使用案例:
<button id="btn">走你</button> <div id="text"></div> <div id="text2"></div>
var obj = new PublishSubscribeMode(); let btn = document.getElementById('btn'); let text = document.getElementById('text'); let text2 = document.getElementById('text2'); let count = 0; let liudehua = function (count) { text.innerText = '刘德华' + count; } let zhangxueyou = function (count) { text2.innerText = '张学友' + count; } obj.subscribe('add', liudehua); // 刘德华订阅add事件 obj.subscribe('add', zhangxueyou); // 张学友订阅add事件 obj.removeSubscribe('add', liudehua); // 刘德华取消订阅add btn.onclick = function () { obj.publish('add', count++); // 点击按钮发布add事件 }