【前端面试】当面试官叫你手写一个JavaScript发布订阅模式

引言

在之前我们学习了Vue中组件通信的三种方式,今天我们要来聊的内容是JS设计模式中的发布订阅模式,对此其实你在子组件与父组件通信中你已经有了初步的概念与认识,无非就是订阅者订阅一个事件,当发布者发布这个事件后,订阅者会执行相应的动作。

在前端领域中,我们不只是单纯的切页面,其中更值得推敲的是理解其中的设计模式,比如我们今天要来聊的就是发布订阅模式它是一种卓越的工具,精髓就在于优雅地解耦应用程序中的组件,使其更加灵活和可扩展。通俗来讲就是将组件之间的关联性削弱,以此来提高系统的可维护性和可扩展性。

Event & CustomEvent

在 JavaScript 中,Event 对象用于处理和响应在 DOM 中发生的事件。 这些事件是用户与页面交互或文档结构发生变化时的响应机制,例如click、input、mouseenter、mouseleave等等。

对于这类普通事件我们不需要自己来定义,我们只需要负责捕获到相应的DOM节点,然后添加监听事件就可以了。CustomEvent是一种自定义事件,它是继承自 Event 的对象,允许我们创建和触发自定义事件。 与普通的事件不同,自定义事件可以携带额外的数据。

打个通俗易懂的比方,想象你在直播平台上关注了一个主播:

  • Event(事件): 就像开启了订阅功能一样,当主播开播时,你会收到一个通用的"开播"通知。你只需关注收到的消息,点击进入直播间。
  • CustomEvent(自定义事件): 假设你是主播,你想要发送更个性化的通知,比如一个特别活动开始。这时,你可以创建一个自定义事件,我们就叫做"特别活动开始",并携带一些自定义参数,如活动主题。当你开始这个特别活动时,触发这个自定义事件,粉丝会收到更详细的通知,包含活动信息。

一言蔽之,Event 是官方提供的默认事件,而 CustomEvent 是主播自定义的通知服务,可以使通知更具体和丰富。 Event 是通用的,CustomEvent 是更个性化的。

事件传播机制 和 取消事件默认行为

在此之前,为了大家接下来能够理解举的例子,我们还需要来简单了解一下Event事件中两种属性:1.事件传播机制 2.取消事件默认行为

  1. 事件传播机制

Event 对象涉及到事件的传播机制,这是指事件如何从文档树的根节点传递到目标元素,然后再从目标元素传播回根节点。这一过程分为三个阶段:捕获阶段、目标阶段和冒泡阶段。

  • 捕获阶段(Capture Phase): 事件从根节点向目标元素传递,途中的元素可以捕获事件。
  • 目标阶段(Target Phase): 事件到达目标元素,触发与目标元素关联的事件监听器。
  • 冒泡阶段(Bubble Phase): 事件从目标元素向根节点再次传递,途中的元素可以响应事件。

Event 对象的 bubbles 属性表示事件是否在冒泡阶段传播,默认为 false

  1. 取消事件默认行为

Event 对象的另一个重要特性是能够取消事件的默认行为。在某些情况下,当特定事件发生时,浏览器会执行与之相关的默认行为,例如点击链接时跳转页面。通过 preventDefault 方法,可 以阻止事件的默认行为:

js 复制代码
document.getElementById('myLink').addEventListener('click', function(event) {
    event.preventDefault();
    console.log('Link clicked, but default action prevented.');
});

在上述例子中,比如我们有一个 idmyLink跳转链接,当链接被点击时,并不会发生跳转,因为我们取消了事件的默认行为,事件监听器阻止了默认的页面跳转行为。

发布订阅例子

那么现在我们已经清楚了基本事件和自定义事件的区别,接下来看看如何通过几个简单的自定义事件例子实现发布订阅吧~

自定义发布订阅

html 复制代码
<div id="box"></div>

    <script>
        let ev = new CustomEvent('look', { bubbles: true, cancelable: false }); // 创建一个支持冒泡且不能取消的look事件
        window.addEventListener('look',() => {
            console.log('在window触发了look事件');
        })

        box.dispatchEvent(ev); // 在box上发布look事件 dispatchEvent是JS自带的构造方法------发布事件
    </script>

代码解释:

首先,我们需要有一个事件的挂载对象,于是我们创建一个DOM节点,id为box

然后我们创建一个自定义事件实例对象ev 并且这个事件名字为look,然后添加了两条自定义的属性:支持冒泡且不能取消,于是这个事件我们就编写完了。

当我们在全局订阅这个事件,并且编写触发回调函数打印语句console.log('在window触发了look事件'),于是我们的订阅也写完了

最后我们在box上发布look事件,当我们打开浏览器控制台就可以看见打印结果了。

自定义发布订阅且携带参数

html 复制代码
<body>
    <div class="box"></div>

    <script>
        let myEvent = new CustomEvent('run',{ detail: {name:'running'}, 'bubbles':true, 'cancelable': false});
        window.addEventListener('run', e => { // 订阅 run 事件
            console.log(`事件被${e.detail.name}触发`);
        })

        window.dispatchEvent(myEvent); // 派发 run 事件
    </script>
</body>

代码解释:

同样的,我们创建一个自定义事件,名字为run,支持冒泡且不能取消,额外的我们设置了一个属性detail(包含{name:'running'}的对象),这里我们的事件就编写完了。

接着,我们在全局订阅这个run事件,并且编写回调函数,传入参数e,当run事件发布时,触发回调函数打印详细信息:事件对象中detail属性里面的name属性,于是我们的订阅就写完了。

最后我们在全局派发run事件,在浏览器控制台就可以看到事件被${e.detail.name}触发了。

通过发布订阅实现异步

欸?有小伙伴感觉有点惊讶,发布订阅模式也可以实现异步?

没错!让我们来看看下面这个例子:

js 复制代码
function fnA() {
            setTimeout(() => {
                console.log('请求A完成');
                window.dispatchEvent(finish) // 派发 finish 事件
            }, 1000);
        }
function fnB() {
            setTimeout(() => {
                console.log('请求B完成');
            }, 500);
        }

比如我们有两个定时器函数fnAfnB,按照JS引擎的执行顺序应该是先打印'请求B完成'再打印'请求A完成',可是我们如果想要想让A打印再打印B呢?

这时候我们其实还可以借助发布订阅来实现异步:

js 复制代码
let finish = new CustomEvent('finish', { detail: { name: 'ok' }, 'bubbles': true, 'cancelable': false });
    
        function fnA() {
            setTimeout(() => {
                console.log('请求A完成');
                window.dispatchEvent(finish) // 派发 finish 事件
            }, 1000);
        }
        function fnB() {
            setTimeout(() => {
                console.log('请求B完成');
            }, 500);
        }
    
        fnA()
        window.addEventListener('finish', () => { // 订阅 finish 事件
            fnB();
        }) 

代码解释:

首先我们定义一个自定义事件名字为finish,并且支持冒泡且不可取消,然后我们在全局给这个事件添加监听事件,也就是订阅了finish这个事件,如果finish事件发布,我们就执行fnB

那么我们何时发布finish事件呢?没错!在fnA函数中,当fnA定时器执行完毕,我们就发布finish事件。

最后我们在全局调用fnA,再来控制台看看输出结果:

面试考题------手写发布订阅

现在我们开始今天的重头戏------面试中会遇到的考题:手写发布订阅

前面的铺垫是为了让大家能够更好的回顾理解JS中发布订阅这个概念,最重要的是我们自己可否手写一个发布订阅封装函数,那么话不多说,上才艺!

我们要实现的功能就是,手写一个事件,然后实现可以实现订阅该事件、只订阅一次该事件以及取消订阅该事件。

js 复制代码
class EventEmitter {
    constructor() {
        this.event = {} // 'run':[fn] 
    }
    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) // 过滤掉 cb 并以新的数组形式返回
        }
    }
}

let ev = new EventEmitter();

const fn1 = (...args) => {
    console.log(...args,'有人调用了fn1');
}
const fn2 = (...args) => {
    console.log(...args,'有人调用了fn2');
}
const fn3 = (...args) => {
    console.log(...args,'有人调用了fn3');
}

ev.on('run', fn1) // 订阅 run 事件 并且执行fn1函数
ev.once('run', fn2) // 只订阅一次 run 事件 并且执行fn2函数

ev.on('run', fn3)
ev.off('run',fn3) // 取消订阅run事件

ev.emit('run', '这是第一次派发事件')
ev.emit('run', '这是第二次派发事件')
ev.emit('run', '这是第三次派发事件')

发布订阅源码解释:

1. 首先我们定义了一个EventEmitter类,用于实现JavaScript中的事件发射器。

2. 然后我们通过构造函数初始化事件对象。在这个对象里面我们存放的是键值对------"事件名称":[对应的函数]

3. on方法添加订阅事件和回调函数,once方法用于订阅一次事件,emit方法用于触发事件并调用对应的回调函数,off方法用于取消订阅事件。

示例代码解释:

1. 首先我们实例化一个EventEmitter对象ev,这个对象可以自由选择订阅或者订阅一次、取消订阅某事件。

2. 然后我们定义了三种方法,用来供订阅者选择事件发布后执行的动作。

3. 接着,ev这个对象订阅了run这个事件并且执行fn1函数,然后只订阅一次run事件并且执行fn2函数,而ev随之订阅了run事件,但是却取消了订阅也就可想而知并不会生效了。

4. 最后,我们开始在ev里面派发事件,总共派发了三次,并且携带了相应的参数,我们现在就可以来看下运行的结果了:

总结

在这篇文章中,我们首先了解了JS里面两种事件机制,以及自定义事件如何携带参数实现发布订阅,最后我们手搓了一个发布订阅模块,并且通过实例演示了发布订阅的效果。

那么到了这里我们今天的文章就结束啦~

创作不易,如果感觉这个文章对你有帮助的话,点个赞吧♥

更多内容【AIGC】如何使用Autogen库打造智能对话体验?请看保姆级教学

ReacheMe : GitHub Gitee

相关推荐
万物得其道者成4 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白5 分钟前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风17 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom29 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan32 分钟前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^41 分钟前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang1 小时前
vue3中开发引导页的方法
开发语言·前端·javascript
WG_171 小时前
C++多态
开发语言·c++·面试
鱼跃鹰飞1 小时前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js