观察者模式中,Subject 直接触及到订阅者(自己维护观察者列表进行注册和通知)。
发布订阅模式则是不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作。
实现一个事件总线 mitt / eventBus
mitt 的使用:
- 创建一个 mitt
import mitt from 'mitt'
const emitter = mitt()
- 订阅事件
emitter.on('foo', callback)
- 发布事件
// params 指事件被触发时回调函数接收的入参
emitter.emit('foo', params)
函数式实现:
function mitt() {
const handlers = {};
return {
handlers,
on(eventName, callback) {
if (!handlers[eventName]) {
handlers[eventName] = [];
}
handlers[eventName].push(callback);
},
emit(eventName, ...args) {
if (handlers[eventName]) {
handlers[eventName].slice().forEach((callback) => callback(...args));
}
},
off(eventName, callback) {
const handlersQueue = handlers[eventName];
const index = handlersQueue.indexOf(callback);
if (index !== -1) {
handlersQueue.splice(index, 1);
}
},
once(eventName, callback) {
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
},
};
}
类实现:
class Mitt {
constructor() {
// handler 是一个事件和回调之间的映射表
this.handlers = {};
}
// on方法用于安装事件监听器
on(eventName, callback) {
if (!this.handlers[eventName]) {
this.handlers[eventName] = [];
}
this.handlers[eventName].push(callback);
}
// emit方法用于触发目标事件
emit(eventName, ...args) {
if (this.handlers[eventName]) {
this.handlers[eventName].slice().forEach((callback) => callback(...args));
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, callback) {
const handlers = this.handlers[eventName];
const index = handlers.indexOf(callback);
if (index !== -1) {
handlers.splice(index, 1);
}
}
// 为事件注册单次监听器
once(eventName, callback) {
// 对回调函数进行包装,使其执行完毕自动被移除
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}
}
与观察者模式对比
观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用。
发布-订阅模式,则是发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。在发布-订阅模式下,实现了完全地解耦。
但这并不意味着,发布-订阅模式就比观察者模式“高级”。 在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦,因此需灵活运用。
参考: