1、发布订阅模式
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
╭─────────────╮ ╭───────────────╮ Fire Event ╭──────────────╮
│ │ Publish Event │ │───────────────>│ │
│ Publisher │────────────────>│ Event Channel │ │ Subscriber │
│ │ │ │<───────────────│ │
╰─────────────╯ ╰───────────────╯ Subscribe ╰──────────────╯
实现思路
- 创建一个对象
- 在该对象上创建一个缓存列表(调度中心)
- on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
- emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
- off 方法可以根据 event 值取消订阅(取消订阅)
- once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
class EventEmitter {
constructor(){
// 维护事件及订阅行为 缓存列表(调度中心)
this.events = {}
}
/**
* 注册事件监听者 (订阅者)
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
on(type,cb){
// 判断是否已存在 不存在创建事件类型
if(!this.events[type]){
this.events[type] = []
}
this.events[type].push(cb);
}
/**
* 注册一次 调用后自动删除
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
once(type,cb){
const _this = this;
function on () {
_this.off(type, on);
cb.apply(_this, arguments);
}
this.on(type, on);
}
/**
* 发布事件 (发布者)
* @param {String} type 事件类型
* @param {...any} args 参数列表,把emit传递的参数赋给回调函数
*/
emit(type, ...args){
// 循环触发已注册的事件
if(this.events[type]){
this.events[type].forEach(element => {
element(...args);
});
}
}
/**
* 取消事件
* @param {String} type 事件类型
* @param Function} cb 回调函数
*/
off(type,cb){
const evenItemData = this.events[type];
if(evenItemData){
// 判断是否存在相同的事件 存在则移除
evenItemData.forEach((item,i)=>{
if(item === cb){
evenItemData.splice(i, 1);
}
})
// 如果没有内容则清楚对象
if(this.events[type].length === 0 ){
delete this.events[type];
}
}
}
}
const eventEmitter = new EventEmitter();
const aFun = (a)=>{
console.log("我是click方法一"+a)
}
const bFun = (b)=>{
console.log("我是click方法二"+b)
}
const cFun = ()=>{
console.log("我是hhhh 注册一次")
}
eventEmitter.on("click",aFun)
eventEmitter.on("click",bFun)
eventEmitter.once("hhhh",cFun)
eventEmitter.emit("click","aaaa");
eventEmitter.emit("hhhh");
eventEmitter.off("click",bFun)
eventEmitter.emit("click","bbbb");
eventEmitter.emit("hhhh"); // 已注销 订阅不到
总结
优点
- 对象之间解耦
- 异步编程中,可以更松耦合的代码编写
- 易理解,可类比于
DOM事件中的dispatchEvent和addEventListener
缺点
- 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱
- 创建订阅者本身要消耗一定的时间和内存
2、观察者模式
观察者模式与发布订阅模式相比,耦合度更高,通常用来实现一些响应式的效果。在观察者模式中,只有两个主体,分别是目标对象Subject,观察者Observer。
- 观察者需
Observer要实现update方法,供目标对象调用。update方法中可以执行自定义的业务代码。 - 目标对象
Subject也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。Subject需要维护自身的观察者数组observerList,当自身发生变化时,通过调用自身的notify方法,依次通知每一个观察者执行update方法。
╭─────────────╮ Fire Event ╭──────────────╮
│ │─────────────>│ │
│ Subject │ │ Observer │
│ │<─────────────│ │
╰─────────────╯ Subscribe ╰──────────────╯
// 观察者
class Observer {
/**
* 构造器
* @param {Function} cb 回调函数,收到目标对象通知时执行
*/
constructor(cb) {
if (typeof cb === 'function') {
this.cb = cb
} else {
throw new Error('Observer构造器必须传入函数类型!')
}
}
/**
* 被目标对象通知时执行
*/
update() {
this.cb()
}
}
// 目标对象
class Subject {
constructor() {
// 维护观察者列表
this.observerList = []
}
/**
* 添加一个观察者
* @param {Observer} observer Observer实例
*/
addObserver(observer) {
this.observerList.push(observer)
}
/**
* 通知所有的观察者
*/
notify() {
this.observerList.forEach(observer => {
observer.update()
})
}
}
const observerCallback = function () {
console.log('我被通知了')
}
const observer = new Observer(observerCallback)
const observer2 = new Observer(observerCallback)
const subject = new Subject();
subject.addObserver(observer);
subject.addObserver(observer2);
subject.notify();
总结
- 角色很明确,没有事件调度中心作为中间者,目标对象
Subject和观察者Observer都要实现约定的成员方法。 - 双方联系更紧密,目标对象的主动性很强,自己收集和维护观察者,并在状态变化时主动通知观察者更新。
差异:
- 在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
- 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
- 观察者模式大多数时候是同步的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
- 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
个人认为,观察者模式和发布订阅模式本质上的思想是一样的,而发布订阅模式可以被看作是观察者模式的一个进阶版。