发布订阅模式
一、认识
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在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
请求的error
、succ
事件。
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);