面试官:聊聊发布订阅模式
什么是发布订阅?
我们在前端开发中会遇到很多种设计模式,例如MVC模式,MVVM模式,单例模式,观察者模式。那么发布订阅模式也是一种非常常见的模式之一。
发布订阅模式允许一个对象(发布者或者称为主题)发布事件,而其他对象(订阅者或者称为观察者)订阅这些事件,当事件发生时,发布者会通知所有订阅者进行相应的处理。这种模式常被用于事件驱动的架构中,如前端开发中的事件处理、消息队列等。
该模式包含三个核心组件:发布者、订阅者、事件。
- 发布者:当发布者发布事件时,会通知所有订阅者,并调用订阅者的处理方法。
- 订阅者:订阅者负责订阅事件或者消息,并提供处理事件的方法。当发布者发布相关事件时,订阅者会接收到通知并执行相应的处理逻辑。
- 事件:事件是发布者和订阅者之间通信的载体,包含了事件类型和相关的数据。
应用场景
1. 事件处理
例如:前端开发中的事件监听和处理,通过发布订阅模式实现事件通知机制。
实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box {
width: 100px;
height: 100px;
background: #000;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
let ev = new Event('look', { bubbles: true,cancelable:false }) //创建一个事件
let box = document.getElementById('box')
box.addEventListener('look', (e) => { //注册或订阅了一个事件,前提要发布
console.log('在box上触发了look事件');
})
box.dispatchEvent(ev) //在box上发布look事件
</script>
</body>
</html>
创建事件对象: 使用 new Event()
创建一个事件对象 ev
。这里的 { bubbles: true, cancelable: false }
参数表示该事件可以冒泡但不可取消。
注册事件监听器(订阅): 使用 addEventListener()
方法注册事件监听器,监听特定类型的事件。
发布事件:
通过 box.dispatchEvent(ev)
在 box
元素上发布了 look
事件,即触发了注册的事件监听器。
发布者是 box
元素,它发布了一个名为 look
的自定义事件。订阅者是通过 addEventListener()
注册在 box
上的事件监听器,当 box
发布 look
事件时,订阅者就会收到通知并执行相应的处理逻辑。
2. 处理异步
实例:
<script>
let finish = new CustomEvent('finish', {detail: {name: 'ok'}})
function fnA() {
setTimeout(() => {
console.log('请求A完成');
window.dispatchEvent(finish)
},1000)
}
function fnB() {
setTimeout(() => {
console.log('请求B完成');
},500)
}
fnA()
window.addEventListener('finish', () => {
fnB()
})
</script>
fnA
函数在 setTimeout 中模拟了一个异步请求,1秒后请求完成,并打印 '请求A完成',然后派发了一个名为finish
的自定义事件到window
对象上。fnB
函数也在 setTimeout 中模拟了一个异步请求,500毫秒后请求完成,并打印 '请求B完成'。- 使用
window.addEventListener
监听finish
事件,当finish
事件被派发时,执行fnB()
,开始执行请求B。
手写发布订阅
完整代码:
class EventEmitter {
constructor() {
this.event = {} // 'run': [cb]
}
on(type, cb) { //用于订阅事件
if(!this.event[type]) {
//没人订阅过
this.event[type] = [cb]
} else {
//有人订阅过
this.event[type].push(cb)
}
}
once(type, cb) { //用于订阅一次事件
const fn = (...args) => {
cb(...args)
this.off(type, fn)
}
this.on(type, fn)
}
emit(type, ...args) { //用于发布事件
//使用类数组接收不定参数
if (!this.event[type]) {
return
} else { //是有订阅者
this.event[type].forEach(cb => {
//在把类数组解构
cb(...args)
})
}
}
off(type,cb) { //取消订阅事件
if (!this.event[type]){
return
} else {
this.event[type] = this.event[type].filter(item => item !== cb);
}
}
}
思路
先在构造函数中初始化了一个空对象 this.event
,用于存储不同类型事件的回调函数列表。且每个类型事件的回调函数都用一个数组来存储。
1. 订阅者(on、once)
on
on
方法用于订阅事件,接收两个参数:事件类型type
和回调函数cb
。- 首先判断该事件类型是否已经有订阅者,如果没有则创建一个数组存储回调函数,并将回调函数添加到数组中;如果已经有订阅者,则直接将回调函数添加到对应的数组中。
once(订阅一次)
注意订阅一次指定是:在订阅一次后,当事件被发布并触发时,该订阅者的回调函数会被执行一次,然后自动取消对该事件的订阅,即使该事件被多次发布也不会再次触发该订阅者的回调函数。
once
方法与 on
方法类似,区别在于它只订阅一次事件。它创建了一个新的函数 fn
,在执行回调函数 cb
后立即取消对该事件的订阅。
2. 发布者(emit)
emit
方法用于发布事件,接收两个参数:事件类型type
和传递给回调函数的参数args
。- 首先判断该事件类型是否有订阅者,如果没有则直接返回;如果有订阅者,则依次执行存储在事件列表中的所有回调函数,并传入参数
args
。
3. 取消订阅 off
off
方法用于取消订阅事件。- 首先判断该事件类型是否有订阅者,如果没有则直接返回;如果有订阅者,则使用
filter
方法过滤掉需要取消的回调函数,从而实现取消订阅。
最后
发布订阅模式可以帮助实现对象之间的解耦,使得不同部分的代码更灵活和可维护。
希望本文对你有帮助!
转载自:https://juejin.cn/post/7355670312396750898