发布订阅模式和观察者模式是开发中常用的设计模式和思想,利用它们可以做到数据更高级的通信,当然在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:
// 通知了!
// 通知了!
可见两个观察者都检测到了被观察者的变化,例子成功!
我的公众号:道道里的前端栈,分享前端知识,嚼碎的感觉真奇妙~