引言
在之前我们学习了Vue中组件通信的三种方式,今天我们要来聊的内容是JS设计模式中的发布订阅
模式,对此其实你在子组件与父组件通信中你已经有了初步的概念与认识,无非就是订阅者订阅一个事件,当发布者发布这个事件后,订阅者会执行相应的动作。
在前端领域中,我们不只是单纯的切页面,其中更值得推敲的是理解其中的设计模式,比如我们今天要来聊的就是
发布订阅模式
。它是一种卓越的工具,精髓就在于优雅地解耦应用程序中的组件,使其更加灵活和可扩展。通俗来讲就是将组件之间的关联性削弱,以此来提高系统的可维护性和可扩展性。
Event & CustomEvent
在 JavaScript 中,Event
对象用于处理和响应在 DOM 中发生的事件。 这些事件是用户与页面交互或文档结构发生变化时的响应机制,例如click、input、mouseenter、mouseleave等等。
对于这类普通事件我们不需要自己来定义,我们只需要负责捕获到相应的DOM节点,然后添加监听事件就可以了。而CustomEvent
是一种自定义事件,它是继承自 Event
的对象,允许我们创建和触发自定义事件。 与普通的事件不同,自定义事件可以携带额外的数据。
打个通俗易懂的比方,想象你在直播平台上关注了一个主播:
- Event(事件): 就像开启了订阅功能一样,当主播开播时,你会收到一个通用的"开播"通知。你只需关注收到的消息,点击进入直播间。
- CustomEvent(自定义事件): 假设你是主播,你想要发送更个性化的通知,比如一个特别活动开始。这时,你可以创建一个自定义事件,我们就叫做"特别活动开始",并携带一些自定义参数,如活动主题。当你开始这个特别活动时,触发这个自定义事件,粉丝会收到更详细的通知,包含活动信息。
一言蔽之,Event 是官方提供的默认事件,而 CustomEvent 是主播自定义的通知服务,可以使通知更具体和丰富。 Event 是通用的,CustomEvent 是更个性化的。
事件传播机制 和 取消事件默认行为
在此之前,为了大家接下来能够理解举的例子,我们还需要来简单了解一下Event事件中两种属性:1.事件传播机制 2.取消事件默认行为
- 事件传播机制
Event
对象涉及到事件的传播机制,这是指事件如何从文档树的根节点传递到目标元素,然后再从目标元素传播回根节点。这一过程分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
- 捕获阶段(Capture Phase): 事件从根节点向目标元素传递,途中的元素可以捕获事件。
- 目标阶段(Target Phase): 事件到达目标元素,触发与目标元素关联的事件监听器。
- 冒泡阶段(Bubble Phase): 事件从目标元素向根节点再次传递,途中的元素可以响应事件。
Event
对象的 bubbles
属性表示事件是否在冒泡阶段传播,默认为 false
。
- 取消事件默认行为
Event
对象的另一个重要特性是能够取消事件的默认行为。在某些情况下,当特定事件发生时,浏览器会执行与之相关的默认行为,例如点击链接时跳转页面。通过preventDefault
方法,可 以阻止事件的默认行为:
js
document.getElementById('myLink').addEventListener('click', function(event) {
event.preventDefault();
console.log('Link clicked, but default action prevented.');
});
在上述例子中,比如我们有一个 id
为 myLink
的跳转链接
,当链接被点击时,并不会发生跳转,因为我们取消了事件的默认行为,事件监听器阻止了默认的页面跳转行为。
发布订阅例子
那么现在我们已经清楚了基本事件和自定义事件的区别,接下来看看如何通过几个简单的自定义事件例子实现发布订阅吧~
自定义发布订阅
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);
}
比如我们有两个定时器函数fnA
和fnB
,按照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里面两种事件机制,以及自定义事件如何携带参数实现发布订阅,最后我们手搓了一个发布订阅模块,并且通过实例演示了发布订阅的效果。