发布订阅模式和观察者模式是开发中常用的设计模式和思想,利用它们可以做到数据更高级的通信,当然在Vue和React等框架中,也用到了它们,本篇就来说一下它们的实现原理并手写代码。
发布订阅模式
原理
在软件架构中,发布-订阅 是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。 —— 维基百科
发布订阅模式从它的概念里就可以看出来特点:发布者 发出的消息,不会发送给特定的 订阅者,订阅者 不会直接接收发布者的消息,反过来也是,这意味着发布者和订阅者不知道彼此的存在。 那他们之间是怎么通信的呢?
原来在它们中间存在一个“第三者”,它被称之为 调度中心 或 事件通道,它维持着 发布者 和 订阅者 之间的联系,过滤所有 发布者 传入的消息并相应地分发它们给 订阅者。
所以现在我们知道了,完成发布订阅的整个流程需要三个角色:
- 发布者
- 调度中心
- 订阅者
实现
在JS中,它们之间的逻辑是这样的,订阅者 向 调度中心 订阅指定的事件,发布者 向 调度中心 发布指定的事件,调度中心 通知 订阅者,订阅者 收到消息,当然一个发布者事件可能会有多个 订阅者。
从这个逻辑里,我们可以列出如下代码:
class EventEmitter{ constructor(){ // 汇总所有的事件和监听 this.listeners = {}; } /** 绑定事件的监听者 * @param {String} eventType 事件类型 * @param {Function} cb 回调函数 */ on(eventType, cb){ // 如果还没有监听者就先初始化一下 if(!this.listeners[eventType]){ this.listeners[eventType] = []; } // 塞入订阅者的回调 this.listeners[eventType].push(cb); } /** 发布事件 * @param {String} eventType 事件类型 * @param {Function} args 参数列表,把emit传递的参数赋给回调函数 */ emit(eventType, ...args){ // 如果已经订阅了事件,就执行 if(this.listeners[eventType]){ this.listeners[eventType].forEach(cb => { cb(...args) }) } } /** 解绑事件的监听者 * @param {String} eventType 事件类型 * @param {Function} cb 回调函数 */ off(eventType, cb){ // 如果当前事件存在监听者,就移除它 if(this.listeners[eventType]){ const index = this.listeners[eventType].findIndex(fn => fn == cb); if(index !== -1){ this.listeners[eventType].splice(index, 1); } if(!this.listeners[eventType].length){ // 如果没有事件监听它了,就直接删除这个事件类型 delete this.listeners[eventType]; } } } }
这样的话,一个简单的发布订阅就实现了,我们就可以这样使用它:
// 实例化一个发布订阅 const ee = new EventEmitter(); // 注册一个监听者 ee.on("speak", function(){ console.log("我讲话了!"); }); ee.emit("speak"); ee.on("speak", function(msg){ console.log(`我说,${msg}`); }); ee.emit("speak","你在干啥?"); // output: // 我讲话了! // 我讲话了! // 我说,你在干啥?
上面打印两次 “我讲话了” 是因为总共注册了2个 “speak” 的监听者,这样一个简易的发布订阅就成功啦!
观察者模式
原理
观察者模式 和 发布订阅模式 不同,观察者模式 是没有 调度中心 的存在的,它是直接监听的对象,当一个对象的状态发生变化时,所有依赖于它的对象都将得到通知,并自动更新,它也是一种一对多的关系。
实现
那没有 调度中心 也就意味着一个对象被直接监听了,此时又得保证在移除的时候可以找到特定的监听者,所以在观察者和被观察者的定义里都需要一个类似唯一id的标识符,我们来下一下它的逻辑:
let obser_ids=0; let obsed_ids=0; // 观察者 class Observer { constructor(){ this.id = obser_ids++; } // 数据发生变化后的回调 update(...args){ console.log(...args) } } // 被观察者 class Observed { constructor(){ this.observers = []; this.id = obsed_ids++; } // 添加观察者 addObserver(observer){ this.observers.push(observer); } // 通知所有观察者 notify(...args){ this.observers.forEach(observer => { observer.update(...args); }); } //移除观察者 deleteObserver(observer){ this.observers = this.observers.filter(o => { return o.id != observer.id; }); } }
观察到变化之后,遍历观察者数组执行回调函数,删除观察者通过唯一标识符判定进行删除,一个简单的观察者就实现了,我们可以测试一下:
// 实例化一个被观察者 let od = new Observed(); // 实例化两个观察者 let or1 = new Observer(); let or2 = new Observer(); // or1 和 or2 观察 od od.addObserver(or1); od.addObserver(or2); // 通知所有观察者 od.notify("通知了!"); // output: // 通知了! // 通知了!
可见两个观察者都检测到了被观察者的变化,例子成功!