本文将介绍发布-订阅模式与观察者模式,他们都属于行为型设计模式。下面我们先来看看“发布-订阅模式”。
发布-订阅模式
它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。
现实中的发布-订阅模式
举一个生活中的例子,我们通过微信对某个感兴趣的公众号进行了订阅,公众号更新了文章,微信会给我们推送更新的消息。这便是现实生活中的发布-订阅模式,我们不用时时刻刻打开微信看这个公众号更新了没有。
DOM 事件
在开发中,我们常给 DOM 绑定事件监听函数,当事件触发时我们绑定的回调函数被调用。
document.body.addEventListener(
"click",
() => {
console.log("点击事件触发");
},
false
);
由于我们无法预知用户在什么时刻点击,就需要监控用户点击document.body的动作。订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息,这是我们在开发中接触到的一种“发布-订阅模式”。这很像公众号订阅的例子,订阅者不知道什么时候公众号什么时候更新内容,于是在订阅后等待微信推送公众号更新的内容。
发布-订阅模式的通用实现
代码如下:
class EventEmitter {
handlers = [];
addListener(eventType, callback) {
if (!this.handlers[eventType]) {
this.handlers[eventType] = [];
}
this.handlers[eventType].push(callback);
}
removeListener(eventType, callback) {
if (!this.handlers[eventType]) {
return;
}
const idx = this.handlers[eventType].findIndex((i) => i === callback);
this.handlers[eventType].splice(idx, 1);
}
emit() {
const eventType = Array.prototype.shift.call(arguments);
const callbacks = this.handlers[eventType];
if (!callbacks || callbacks.length === 0) {
return false;
}
for (let i = 0, cb; (cb = callbacks[i++]); ) {
cb.apply(this, arguments);
}
}
once(eventType, callback) {
// 对回调函数进行包装,使其执行完毕自动被移除
function wrapper() {
callback.apply(this, arguments);
this.removeListener(eventType, wrapper);
}
this.addListener(eventType, wrapper);
}
removeAllListener(eventType) {
if (!this.handlers[eventType]) {
this.handlers[eventType] = [];
}
}
}
调用示例:
const eventEmitter = new EventEmitter();
const cb = (val) => {
console.log("val * 10", val * 10);
};
eventEmitter.addListener("update", cb);
eventEmitter.emit("update", 10);
eventEmitter.removeListener("update", cb);
eventEmitter.emit("update", 20);
观察者模式
在我们的开发工作中,常有这样的情况。突然被领导拉去参加需求评审会,评审会后产品经理根据最终的评审结果修改需求文档,研发们也都知道了有个开发任务要开始了,等着出最新的需求文档后干活。几天后产品经理拉了个群,将需求文档发到群里并艾特相关的研发,研发收到文件后开始干活。这种发布者直接触及到订阅者的操作,叫观察者模式。
但如果产品经理没有拉群,而是把需求文档上传到了公司的需求平台上,需求平台感知到文件的变化、邮件通知了每一位订阅了该需求的开发者,这种发布者不直接触及到订阅者、而是由第三方来完成实际的通信的模式,叫做发布-订阅模式。
经过对比不难看出两种模式的区别在于是否存在第三方,由第三方完成实际通信。可以通过下图来直观的感受一下:
观察者模式的实现
从上面举的例子中,可以看出观察者模式最重要的两个角色是发布者(例子中的产品经理)和订阅者(研发)。发布者有着管理订阅者以及通知订阅者的能力,订阅者则需要具备接收通知的能力。
我们来实现发布者(Publisher)类:
class Publisher {
constructor() {
// 订阅者列表
this.observers = [];
}
// 增加订阅者
add(observer) {
this.observers.push(observer);
}
// 移除订阅者
remove(observer) {
this.observers.forEach((item, idx) => {
if (item === observer) {
this.observers.splice(idx, 1);
}
});
}
// 通知所有订阅者
notify() {
this.observers.forEach((observer) => {
observer.update(this);
});
}
}
再来实现订阅者(Observer)类:
class Observer {
// update 方法用来接收通知
update(publisher) {}
}
至此,我们完成了通用的发布者和订阅者类的设计。面对不同的业务场景,我们可以基于这两个类拓展,去完成复杂功能的开发。下面我们基于这两个类来拓展出上文中提到的开发经理的例子。
我们首先来实现开发经理的类:
class PMPublisher extends Publisher {
constructor() {
super();
// 初始化需求文档
this.prdInfo = null;
}
// 获取当前的需求文档的方法
getPrdInfo() {
return this.prdInfo;
}
// 更新需求文档
setPrdInfo(info) {
this.prdInfo = info;
// 需求文档更新,通知所有群成员
this.notify();
}
}
再实现研发的类:
class DeveloperObserver extends Observer {
constructor(name) {
super();
this.prdInfo = null;
this.name = name;
}
// 覆写父类的 update 方法
update(publisher) {
// 获取最新的需求文档
this.prdInfo = publisher.getPrdInfo();
console.log(`${this.name}收到文档`, this.prdInfo);
// 开始搬砖
this.work();
}
// 具体的工作
work() {
console.log("开始后疫情时代背景下的工作...");
}
}
最后是整个流程串起来:
const bugTerminator = new DeveloperObserver("BUG终结者");
const pm = new PMPublisher();
// 需求文档
const prd = { name: "需求XXX" };
// pm 开始拉群,把“BUG终结者”拉进群
pm.add(bugTerminator);
// pm 发送需求文档,并艾特了所有人
pm.setPrdInfo(prd);
以上,就是观察者模式的代码实现了。
参考文章,感兴趣的小伙伴可以看看:
纸质书籍:JavaScript 设计模式与开发实践