跳到主要内容

发布订阅模式

一、认识


发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript中,我们一般用事件模型来替代传统的发布-订阅模式

发布-订阅模式中,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者(Subscriber)

二、特点


2.1 优点

  • 对象间功能解耦,弱化对象间的引用关系;
  • 更细粒度地管控,分发指定订阅主题通知

2.2 缺点

  • 时间间解耦后,代码阅读不够直观,不易维护;
  • 额外对象创建,消耗时间和内存(很多设计模式的通病)

三、对比


Preview

3.1 观察者模式

  • 观察者模式虽然实现了对象间依赖关系的低耦合,但却不能对事件通知进行细分管控,而且是统一通知,不能按需通知,或者自定义通知事件(就是谁需要通知谁)
  • 观察者模式主题观察者是互相感知的,观察者(Observer)直接订阅(Subscribe)``主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件

3.2 发布-订阅模式

  • 发布-订阅模式是借助第三方来实现调度的,发布者订阅者之间互不感知(“第三者” (事件中心)出现。目标对象并不直接通知观察者,而是通过事件中心来派发通知。)订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

  • 发布订阅模式中,组件是松散耦合

四、实现


4.1 Vue 方案

class PubSub {
constructor() {
this.events = {};
}
subscribe(event, fn) {
if (Array.isArray(event)) {
event.forEach((e) => {
this.subscribe(e, fn);
});
} else {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(fn);
}
}
unsubscribe(event, fn) {
if (Array.isArray(event)) {
event.forEach((e) => {
this.unsubscribe(e, fn);
});
}
const cbs = this.events[event];
if (!cbs) {
return;
}
if (!fn) {
this.events[event] = null;
}
let cb;
let i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn) {
cbs.splice(i, 1);
break;
}
}
}
publish(event, ...args) {
let cbs = this.events[event];
if (cbs) {
cbs.forEach((cb) => {
cb.apply(this, args);
});
}
}
subScribeOnce(event, fn) {
function on() {
this.unsubscribe(event, on);
fn.apply(this, arguments);
}
this.subscribe(event, on);
}
}

const pubSub = new PubSub();

function handlerEventA(name) {
console.log("handlerEventA", name);
}
function handlerEventB(name) {
console.log("handlerEventB", name);
}
function handlerEventC(name) {
console.log("handlerEventC", name);
}

pubSub.subscribe("eventA", handlerEventA);
pubSub.subscribe("eventB", handlerEventB);
pubSub.subScribeOnce(["eventA", "eventB"], handlerEventC);

pubSub.publish("eventA", "eventA - 哈哈");
pubSub.publish("eventA", "eventA - 嘻嘻");
pubSub.publish("eventB", "eventB - 哈哈");
pubSub.publish("eventB", "eventB - 嘻嘻");

测试用例

const pubSub = new PubSub();

function handlerEventA(name) {
console.log("handlerEventA", name);
}
function handlerEventB(name) {
console.log("handlerEventB", name);
}
function handlerEventC(name) {
console.log("handlerEventC", name);
}

pubSub.subscribe("eventA", handlerEventA);
pubSub.subscribe("eventB", handlerEventB);
pubSub.subScribeOnce(["eventA", "eventB"], handlerEventC);

pubSub.publish("eventA", "eventA - 哈哈");
pubSub.publish("eventA", "eventA - 嘻嘻");
pubSub.publish("eventB", "eventB - 哈哈");
pubSub.publish("eventB", "eventB - 嘻嘻");

4.2 网上方案

class PubSub {
// 调度中心
constructor() {
this.eventObj = {};
this.callbackId = 0;
}
// 订阅
subscribe(name, callback) {
if (!this.eventObj[name]) {
this.eventObj[name] = {};
}
const id = this.callbackId++;
this.eventObj[name][id] = callback;
return id;
}
// 订阅一次
subScribeOnce(name, callback) {
if (!this.eventObj[name]) {
this.eventObj[name] = {};
}
const id = "D" + this.callbackId++;
this.eventObj[name][id] = callback;
return id;
}
// 发布
publish(name, ...args) {
const eventList = this.eventObj[name];
for (const id in eventList) {
eventList[id](...args);
if (id.indexOf("D" !== "-1")) {
delete eventList[id];
}
}
}
// 取消订阅
unsubscribe(name, id) {
delete this.eventObj[name][id];
if (!Object.keys(this.eventObj[name]).length) {
delete this.eventObj[name];
}
}
}

测试用例

const pubsub = new PubSub();
pubsub.subscribe("a", (args) => {
console.log("订阅事件a-1", args);
});
pubsub.subscribe("a", (args) => {
console.log("订阅事件a-2", args);
});
pubsub.subscribe("b", (args) => {
console.log("订阅事件b-1", args);
});
pubsub.subscribe("b", (args) => {
console.log("订阅事件b-2", args);
});

pubsub.publish("a", "哈哈");
pubsub.publish("b", "嘻嘻");

五、场景


5.1 用于异步编程

发布-订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅ajax请求的errorsucc 事件。

5.2 DOM 事件监听

订阅loginBtn元素上的click事件,当loginBtn元素被点击时,loginBtn会向订阅者发布这个消息。

 *let loginBtn = document.getElementById('#loginBtn');
// 监听回调函数(指定事件)
function notifyClick() {
console.log('我被点击了');
}
// 添加事件监听
loginBtn.addEventListener('click', notifyClick);
// 触发点击, 事件中心派发指定事件
loginBtn.click();
// 取消事件监听
loginBtn.removeEventListener('click', notifyClick);

参考资料


面试官:请手写一个EventBus,让我看看你的代码能力!