由浅入深手撕发布订阅模式

浅聊一下

掘友们平时逛掘金逛的比较多,那免不了会关注一些大佬,那么当我们关注的大佬发布动态的时候,我们会在关注里第一时间就接受到,这种掘友关注大佬,大佬发布动态,掘友接受动态的模式像极了我们今天要来聊的发布订阅模式...话不多说,进入正题

一个简单的发布订阅

其实我们平时用过的监听器就类似一个发布订阅

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事件
  1. 首先定义一个事件

我们new了一个Event,'look'代表你的事件的名字,你也可以理解为你在掘金上订阅的大佬的名字

  • bubbles cancelable

bubbles属性记录是否可以冒泡,如果值为true,那么事件也会冒泡到订阅的DoM节点的父容器,所以当订阅的子容器触发事件时,父容器也会触发,反之,则不会触发

cancelable属性记录是否可以取消订阅,若取消了订阅,那就跟没订阅一样,当大佬发动态时,你也不会接收到

2.订阅事件

这里使用监听器订阅了look事件,当事件被触发时,如果事件是可取消的(即cancelabletrue),则调用event.preventDefault()取消事件的默认行为;否则,在控制台打印出"在box上触发了look事件"。

  1. 发布订阅

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紧接着运行,解决异步问题

结尾

掘友们,发布订阅模式要达到闭着眼睛都能写出来的水平啊!!!

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
大圣数据星球3 小时前
Fluss 写入数据湖实战
大数据·设计模式·flink
思忖小下4 小时前
梳理你的思路(从OOP到架构设计)_设计模式Template Method模式
设计模式·模板方法模式·eit
LCG元6 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar10 小时前
纯前端实现更新检测
开发语言·前端·javascript
GISer_Jing10 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_7482455210 小时前
吉利前端、AI面试
前端·面试·职场和发展