浅聊一下
掘友们平时逛掘金逛的比较多,那免不了会关注一些大佬,那么当我们关注的大佬发布动态的时候,我们会在关注里第一时间就接受到,这种掘友关注大佬,大佬发布动态,掘友接受动态的模式像极了我们今天要来聊的发布订阅模式...话不多说,进入正题
一个简单的发布订阅
其实我们平时用过的监听器就类似一个发布订阅
html
<button id="myButton">点击我</button>
js
// 获取按钮元素
var button = document.getElementById("myButton");
// 添加点击事件监听器
button.addEventListener("click", function() {
console.log("按钮被点击了!");
});
这个例子中,按钮元素充当了事件的发布方,而事件监听器则充当了事件的订阅方。
当用户点击按钮时,事件被发布出去,监听器接收到事件并进行相应的处理。
这符合发布/订阅模式的思想。
写一个自己的发布订阅
我们怎么样来写一个自己的发布订阅,不使用早就定义好的点击事件'click'等等
html
<div id="box"></div>
创建了一个id为box的div
js
let ev = new Event('look',{bubbles:true,cancelable:true});//定义事件
let box = document.getElementById('box')
box.addEventListener('look',(event)=>{
if(event.cancelable){
event.preventDefault()
}else{
console.log('在box上触发了look事件');
}
})
box.dispatchEvent(ev)//在box上发布look事件
- 首先定义一个事件
我们new了一个Event,'look'代表你的事件的名字,你也可以理解为你在掘金上订阅的大佬的名字
- bubbles cancelable
bubbles属性记录是否可以冒泡,如果值为true,那么事件也会冒泡到订阅的DoM节点的父容器,所以当订阅的子容器触发事件时,父容器也会触发,反之,则不会触发
cancelable属性记录是否可以取消订阅,若取消了订阅,那就跟没订阅一样,当大佬发动态时,你也不会接收到
2.订阅事件
这里使用监听器订阅了look事件,当事件被触发时,如果事件是可取消的(即cancelable
为true
),则调用event.preventDefault()
取消事件的默认行为;否则,在控制台打印出"在box上触发了look事件"。
- 发布订阅
box.dispatchEvent(ev)就相当于大佬发布动态了,这时关注了大佬的掘友们都会收到动态
面试题:手写一个发布订阅模式
在面试的时候,发布订阅模式是一个难点,面试官会给你以下一段代码,叫你补全
js
class EventEmitter{
constructor(){}
on(){}
once(){}
emit(){}
off(){}
}
我们先来分析一下:
- constructor
类的构造函数,用于初始化实例。
- on
事件监听方法,用于注册事件监听器。该方法可能接受事件类型和回调函数等参数,以便在事件被触发时执行相应的回调函数。
- once
一次性事件监听方法,类似于on()
,但是注册的监听器只会在第一次触发事件时执行,之后会自动移除。
- emit
事件触发方法,用于触发已注册的事件监听器。该方法可能接受事件类型和参数等参数,以便将事件传递给对应的监听器执行。
- off
事件移除方法,用于移除已注册的事件监听器。该方法可能接受事件类型和回调函数等参数,以便从事件监听列表中移除对应的监听器。
补全代码
js
class EventEmitter{
constructor(){
this.event = {}//'run':[fun]
}
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);
}
}
}
- constructor
在里面,创建了一个空对象用来保存事件类型和对应的回调函数。
- on
事件监听方法,用于注册事件监听器。首先,判断是否已经存在该事件类型的监听器数组,如果不存在,则创建一个新的数组,并将回调函数cb
添加到数组中;如果已经存在,则将回调函数cb
追加到已有的监听器数组中。
- off
事件取消订阅方法,用于移除已注册的事件监听器。首先,判断是否存在该事件类型的监听器数组,如果不存在,直接返回;如果存在,使用filter()
方法过滤掉与传入的回调函数cb
相等的监听器,并更新监听器数组。
- once
一次性事件监听方法,类似于on()
,但是注册的监听器只会在第一次触发事件时执行,并在执行后自动从监听器数组中移除。为了实现这个功能,你创建了一个临时函数fn
,它会在触发事件时执行回调函数cb
,然后再通过off()
方法将自身从监听器数组中移除。最后,调用on()
方法注册这个临时函数作为监听器。
- emit
事件触发方法,用于派发已注册的事件。首先,判断是否存在该事件类型的监听器数组,如果不存在,直接返回;如果存在,遍历监听器数组,并依次执行每个监听器的回调函数,并传入相应的参数args
。
Testing
js
let ev = new EventEmitter();
const fn1 = (a,b)=>{
console.log(a,b,'fn1');
}
const fn2 = (a,b)=>{
console.log(a,b,'fn2');
}
const fn3 = (a,b)=>{
console.log(a,b,'fn3');
}
ev.on('run',fn1)
ev.once('run',fn2)
ev.on('run',fn3)
ev.off('run',fn3)
ev.emit('run',1,2)
ev.emit('run',1,3)
ev.emit('run',1,4)
在上面这段代码中,订阅了fn1,一次订阅了fn2,订阅了fn3又取消了订阅
那么在三次发布订阅的结果应该是:
txt
1 2 fn1
1 2 fn2
1 3 fn1
1 4 fn1
来看结果:
加分项
我们在之前就已经聊到了异步,还不懂的掘友可以去看看我的文章(Promise:解决JavaScript异步编程难题 - 掘金 (juejin.cn)),那么当面试官问你怎么解决异步难题的时候,怎么回答才能加分呢?
不难猜到,那就是用发布订阅模式来解决(虽然平时不会用到)
已知在订阅发布以后,订阅者才会收到消息,所以我们也可以用来解决异步问题
js
function fnA(){
setTimeout(()=>{
console.log('请求A完成');
let ev = new CustomEvent('finish',{detail:{name:'ok'}})
window.dispatchEvent(ev)
},1000)
}
function fnB(){
setTimeout(()=>{
console.log('请求B完成');
},500)
}
fnA()
window.addEventListener('finish',()=>{
fnB()
})
我们让window订阅一个finish事件,当finish发布以后,再触发fnB,当fnA完成时派发finish,于是fnB紧接着运行,解决异步问题
结尾
掘友们,发布订阅模式要达到闭着眼睛都能写出来的水平啊!!!