被考察很多次的发布订阅模式
在观察大佬们的面经时,常常提起“发布订阅模式”、“手写 EventEmitter”,遂学习,记录于此,对这篇文章有大量(搬运)参考
介绍
发布订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到状态改变的通知。
发布订阅模式中,包含发布者,事件调度中心,订阅者三个角色。发布者和订阅者是松散耦合的,互不关心对方是否存在,他们关注的是事件本身。发布者借用事件调度中心提供的publish
方法发布事件,而订阅者则通过subscribe
进行订阅。
特点
- 发布订阅模式中,对于发布者
Publisher
和订阅者Subscriber
没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。 - 松散耦合,灵活度高,常用作事件总线。
- 易理解,可类比于
DOM
事件中的dispatchEvent
和addEventListener
。
缺点
- 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。
和观察者模式的区别
定性上:观察者是经典软件设计模式
中的一种,但发布订阅只是软件架构中的一种消息范式
。
组成上:观察者模式本身只需要2个
角色便可成型,即观察者
和被观察者
,其中被观察者
是重点。而发布订阅需要至少3个
角色来组成,包括发布者
、订阅者
和发布订阅中心
,其中发布订阅中心
是重点。
实现上:
观察者模式一般至少有一个可被观察的对象 Subject,可以有多个观察者
去观察这个对象。二者的关系是通过被观察者主动
建立的,被观察者
至少要有三个方法——添加观察者、移除观察者、通知观察者。
当被观察者将某个观察者添加到自己的观察者列表
后,观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发通知观察者
方法时,观察者即可接收到来自被观察者的消息。
被观察者是要有一个数组来容纳所有的观察者!
与观察者模式相比,发布订阅核心基于一个中心来建立整个体系。其中发布者和订阅者不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。
> 关联:发布订阅的实现内部利用了观察者模式
,订阅者
和发布订阅中心
的关系类似观察者
和被观察者
的关系。
简单实现
class PubSub {
constructor() {
// 维护事件及订阅行为
this.events = {}
}
/**
* 注册事件订阅行为
* @param {String} type 事件类型
* @param {Function} callback 回调函数
*/
subscribe(type, callback) {
if (!this.events[type]) {
this.events[type] = []
}
this.events[type].push(callback)
}
/**
* 发布事件
* @param {String} type 事件类型
* @param {...any} args 参数列表
*/
publish(type, ...args) {
if (this.events[type]) {
this.events[type].forEach(callback => {
callback(...args)
})
}
}
/**
* 移除某个事件的一个订阅行为
* @param {String} type 事件类型
* @param {Function} callback 回调函数
*/
unsubscribe(type, callback) {
if (this.events[type]) {
const targetIndex = this.events[type].findIndex(item => item === callback)
if (targetIndex !== -1) {
this.events[type].splice(targetIndex, 1)
}
if (this.events[type].length === 0) {
delete this.events[type]
}
}
}
/**
* 移除某个事件的所有订阅行为
* @param {String} type 事件类型
*/
unsubscribeAll(type) {
if (this.events[type]) {
delete this.events[type]
}
}
}
手写 EventEmitter
EventEmitter
(事件派发器)是 Node.js
的核心模块 events
中的类,用于对 Node.js
中的事件进行统一管理,用 events
特定的 API
对事件进行添加、触发和移除等等,EventEmitter
的核心就是事件触发与事件监听器功能的封装。
简而言之,EventEmitter
就是一个典型的发布订阅模式,实现了事件调度中心。
实现:
class EventEmitter {
constructor() {
// 维护事件及监听者
this.listeners = {}
}
/**
* 注册事件监听者
* @param {String} type 事件类型
* @param {Function} callback 回调函数
*/
on(type, callback) {
if (!this.listeners[type]) { // 如果该事件类型不存在
this.listeners[type] = [] // 为该事件类型设置数组,存放回调函数
}
this.listeners[type].push(callback) // 将回调函数放入该事件类型数组
}
/**
* 发布事件
* @param {String} type 事件类型
* @param {...any} args 参数列表,把 emit 传递的参数赋给回调函数
*/
emit(type, ...args) {
if (this.listeners[type]) { // 如果该事件类型存在
this.listeners[type].forEach(callback => {
callback(...args) // 调用该事件类型数组中的每一个回调函数,并传入参数
})
}
}
/**
* 移除某个事件的一个监听者
* @param {String} type 事件类型
* @param {Function} callback 回调函数
*/
off(type, callback) {
if (this.listeners[type]) {
// 查询传入回调函数在该事件类型数组中的下标,并将其下标用 targetIndex 存储
const targetIndex = this.listeners[type].findIndex(item => item === callback)
if (targetIndex !== -1) { // 说明该回调函数存在于事件类型数组中
this.listeners[type].splice(targetIndex, 1) // 删除该回调函数
}
if (this.listeners[type].length === 0) { // 该事件类型数组为空
delete this.listeners[type] // 删除该事件类型
}
}
}
/**
* 移除某个事件的所有监听者
* @param {String} type 事件类型
*/
offAll(type) {
if (this.listeners[type]) { // 如果该事件类型数组存在
delete this.listeners[type] // 直接删除该事件类型
}
}
}
使用:
// 创建事件管理器实例
const ee = new EventEmitter()
// 注册一个imagine事件监听者
ee.on('imagine', function() { console.log('前端收割机') })
// 发布事件imagine
ee.emit('imagine')
// 前端收割机
// 也可以emit传递参数
ee.on('imagine', function(name,address) { console.log(`大家好,我是${name},我来自${address}!`) })
ee.emit('imagine', '前端收割机','广东') // 此时会打印两条信息,因为前面注册了两个imagine事件的监听者
// 前端收割机
// 大家好,我是前端收割机,我来自广东!
// 测试移除事件监听
const BeRemovedListener = function() { console.log('我是一个可以被移除的监听者') }
// 注册一个TestOff事件监听者
ee.on('TestOff', BeRemovedListener)
// 发布事件TestOff
ee.emit('TestOff')
// 我是一个可以被移除的监听者
// 移除事件监听
ee.off('TestOff', BeRemovedListener)
ee.emit('TestOff') // 此时事件监听已经被移除,不会再有console.log打印出来了
// 测试移除imagine的所有事件监听
ee.offAll('imagine')
console.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送imagine事件也不会有反应了