定义
发布订阅模式又叫观察者模式。它定义对象间的一对多的依赖关系,当一个对象的状态发生变化时,所有订阅它的对象都将收到通知。js 中我们一般用事件模型来代替传统的观察者
应用场景
观察者模式在实际中有很多的应用场景,比如:
- 房产中介发的看房通知,中介是发布者,而你是订阅者。
- js 中的 DOM 事件
- Node 中的 EventEmitter 类
下面我们就实现一个自定义的 EventEmitter, 为了方便查看,我们只实现增加监听器和触发监听器的功能
class EventEmitter {
constructor() {
this.listenersMap = {};
}
// 监听器
on(eventName, listener) {
this.listenersMap.hasOwnProperty(eventName)
? this.listenersMap[eventName].push(listener)
: (this.listenersMap[eventName] = [listener]);
return this;
}
// 添加单次监听器 listener 到名为 eventName 的事件。 当 eventName 事件下次触发时,监听器会先被移除,然后再调用。
once(eventName, listener) {
listener._once = true;
this.listenersMap.hasOwnProperty(eventName)
? this.listenersMap[eventName].push(listener)
: (this.listenersMap[eventName] = [listener]);
return this;
}
// 触发
emit(eventName) {
const args = [].slice.call(arguments, 1);
this.listenersMap[eventName] &&
(this.listenersMap[eventName] = this.listenersMap[eventName].filter(
(listener, index) => {
listener.apply(null, args);
return !listener._once;
}
));
return this;
}
off(eventName, fn) {
this.listenersMap[eventName] &&
(this.listenersMap[eventName] = this.listenersMap[eventName].filter(
(listener, index) => {
return listener !== fn;
}
));
}
// listeners
listeners(eventName) {
return this.listenersMap[eventName];
}
}
var myEmitter = new EventEmitter();
// 第一个监听器。
myEmitter.on("event", function firstListener() {
console.log("第一个监听器");
});
// 第二个监听器。
myEmitter.on("event", function secondListener(arg1, arg2) {
console.log(`第二个监听器中的事件有参数 ${arg1}、${arg2}`);
});
// 第三个监听器
myEmitter.on("event", function thirdListener(...args) {
const parameters = args.join(", ");
console.log(`第三个监听器中的事件有参数 ${parameters}`);
});
// 第4个监听器
myEmitter.once("event", function fouthListener(...args) {
const parameters = args.join(", ");
console.log(`once4 ${parameters}`);
});
// 第5个监听器
myEmitter.once("event", function fifthListener(...args) {
const parameters = args.join(", ");
console.log(`once5 ${parameters}`);
});
myEmitter.emit("event", 1, 2, 3, 4, 5); // 第一次调用时,所有的都会执行
myEmitter.emit("event", 1, 2, 3, 4); // 第二次执行时,只执行on注册的监听器,once注册的都已经被清理了
console.log(myEmitter.listeners("event"));
如何实现先发布再订阅呢?
上面的例子中,只能实现先订阅,再发布。但是在实际项目中,如 qq、微信等离线消息。我们需要先将消息保存下来,等到有对象来订阅它们,再重新将消息发布给订阅者。
为了满足这种需求,我们就需要创建一个存放离线事件的堆栈,当事件发布时,如果还没有订阅者。则先通过函数包裹事件,等待有对象订阅时,我们将遍历队列且依次执行包装函数。且离线事件的生命周期只有一次,就像未读消息只会被重新阅读一次
下面,我们来改造上面的类
class EventEmitter {
constructor() {
this.listenersMap = {};
this.emitCache = {}; // 发布事件缓存堆栈
}
// 监听器
on(eventName, listener) {
if (this.emitCache.hasOwnProperty(eventName)) {
this.emitCache[eventName].forEach((listenerWrap) => {
listenerWrap(listener);
});
delete this.emitCache[eventName];
} else {
this.listenersMap.hasOwnProperty(eventName)
? this.listenersMap[eventName].push(listener)
: (this.listenersMap[eventName] = [listener]);
}
return this;
}
// 添加单次监听器 listener 到名为 eventName 的事件。 当 eventName 事件下次触发时,监听器会先被移除,然后再调用。
once(eventName, listener) {
listener._once = true;
this.listenersMap.hasOwnProperty(eventName)
? this.listenersMap[eventName].push(listener)
: (this.listenersMap[eventName] = [listener]);
return this;
}
// 触发
emit(eventName) {
const args = [].slice.call(arguments, 1);
if (this.listenersMap.hasOwnProperty(eventName)) {
this.listenersMap[eventName] &&
(this.listenersMap[eventName] = this.listenersMap[eventName].filter(
(listener, index) => {
listener.apply(null, args);
return !listener._once;
}
));
} else {
this.emitCache[eventName]
? this.emitCache[eventName].push(function listenerWrap(fn) {
fn(args);
})
: (this.emitCache[eventName] = [
function listenerWrap(fn) {
fn(args);
},
]);
}
return this;
}
off(eventName, fn) {
this.listenersMap[eventName] &&
(this.listenersMap[eventName] = this.listenersMap[eventName].filter(
(listener, index) => {
return listener !== fn;
}
));
}
// listeners
listeners(eventName) {
return this.listenersMap[eventName];
}
}
var myEmitter = new EventEmitter();
myEmitter.emit("event", 1, 2, 3, 4, 5); // 第一次调用时,所有的都会执行
myEmitter.on("event", function firstListener() {
console.log("第一个监听器");
});
优点
- 时间上的解耦,即订阅对象不需要烦恼何时去关注信息,只需要等待发布者在合适的时间发布信息并接受即可
- 对象之间的解耦,即订阅对象和发布对象不需要强耦合在一起,当有新的对象需要接受消息时,他只需要订阅即可,发布对象无需关心订阅对象
缺点
- 创建一个订阅对象需要时间和内存,但是可能并不会推送消息。那么这个订阅对象可能一直存在于内存中
- 发布订阅会弱化对象与对象之间的关系,如果过度使用,对象与对象之间的必要联系都会被深埋在背后,导致程序难以理解和维护。尤其是嵌套使用发布订阅,这个时候追踪 bug 变得困难。