如何用发布订阅的思想实现异步
前言
正文
什么是发布订阅
首先,什么是发布订阅?举个很简单的例子,在日常生活中有些人爱喝牛奶,于是就订了牛奶,在门口挂一个箱子,每天都有人送。预定送牛奶的服务以及挂箱子就是订阅。而送奶工送奶的这一行为就是发布。在代码中我们也经常会这么做,例如事件监听器,监听一个点击事件,我们也可以说是订阅了一个点击事件。当点击事件发生,就会触发之后的行为。而不点击自然就不触发。就像送奶工不送奶那订奶人就不喝奶,一送就喝奶,就是这么个道理。
如何实现发布订阅
像之前说的,事件监听器某种程度上来说也算是发布订阅,但如果只能通过事件监听器去实现监听固定的那几个事件,那这也太low了。所以我们需要通过js自带的Event构造函数去自己创造事件。Event构造函数接受两个形参,第一个是字符串,代表了这个事件的名称,第二个是对象,里面包含三个键值对,分别是
· cancelable: 布尔值,表示事件是否可以被取消(即是否可以通过 preventDefault() 方法阻止其默认行为),默认为 false。
· composed: 布尔值,对于Shadow DOM而言,表示事件是否应该穿过shadow根,默认为 false。
例如在这里我就定义了一个名为look的事件,并设置为允许冒泡允许取消。
let ev = new Event('look', { bubbles: true, cancelable: true })
// 创建一个可以冒泡且不能取消的事件,名为look
如此一来,事件就有了,但是我们还差两个东西:监听事件的元素和事件的发布。首先在html部分随便写个容器,然后通过原生js拿到容器并监听look事件,一旦监听到,立马打印一个“在box触发look事件”。
<div id="box"></div>
let box = document.getElementById('box')
box.addEventListener('look', () => {
// 订阅一个look事件
console.log('在box触发look事件');
})
接下来我们就要决定事件在哪里发布,这里我只写了一个容器,那么就在这个容器上发布,发布的方法就是通过去把刚刚实例化的对象作为参数传给dispatchEvent函数。
box.dispatchEvent(ev)
如此一来,发布订阅就算是搞定了,我发布几次,监听的元素就会触发几次,不发布就不会触发函数。
CustomEvent
相比于Event构造函数,CustomEvent要更强大一些,它可以在第二个参数中多加一个名为detail的对象去保存自定义的内容,并在后续触发的时候通过个事件监听器内部的回调函数的参数去访问到这个自定义的内容
let myEvent = new CustomEvent('run', { detail: { reader: '彭于晏' }, 'bubbles': true, cancelable: false })
console.log(myEvent);
手写发布订阅
气氛都到这了,不手写怕是要被大佬骂了。根据刚刚提到的,event首先是个构造函数,其次,拥有发布,订阅,取消订阅三个功能,我们再加一个仅订阅一次的功能,也就是订阅事件发生一次过后取消订阅。那么基础部分的代码就出来了
class EventEmitter {
constructor() {
}
on(type, cb) {
// 持续订阅
}
once(type, cb) {
// 只订阅一次
}
emit(type, ...args) {
// 发布事件
}
off(type, cb) {
// 取消订阅
}
}
on方法
接下来,我们先写on方法去实现订阅功能。On方法接受两个参数,第一个是订阅的事件名,第二个是回调函数。这里需要强调的是,可能会有多个对象订阅同一个对象,但是却有不同的回调函数,就好比大家都订A品牌的牛奶,有人拿来喝,有人拿来泡咖啡,还有富哥拿来洗脸(真奢侈。。。),那么我就需要一个东西去存这些回调函数,因为on被调用之后回调可不会被调用,不然还发布个啥?总不能人家不发牛奶你把奶箱子给啃了吧?
所以on的方法只能是拿到回调函数然后存起来。既然每个对象都要存,所以,我们干脆在constructor中添加一个对象,当on被调用的时候,把监听事件的名称作为键,数组作为值,然后按照事件名去把回调函数塞进去。不过这里需要做个判断,就是这个事件之前是否被监听过,如果没有就新建键值对,有的话说明数组已经有了,直接push就好了。
constructor() {
this.event = {}
}
on(type, cb) {
// 持续订阅
if (!this.event[type]) {
this.event[type] = [cb]
} else {
this.event[type].push(cb)
}
}
Off方法
On方法搞定之后,off就容易多了,on是塞进去,那off不就是取出来呗,这样当事件发布时候去触发队友数组里面所有的 回调函数的时候就出发不到要被取消掉的事件了。
off(type, cb) {
// 取消订阅
if (!this.event[type]) {
// 没被订阅过
return
} else {
// 被订阅了,要去掉
this.event[type] = this.event[type].filter(item => item !== cb)
// filter会把满足调教的元素留下,不满足的删掉
}
}
Emit方法
On和off都讲了,那发布简直不要太简单,直接找,对象有这个键,就把这个数组拿过来forEach一遍,然后统统触发就行了。不过我这里用...agrs去设置形参,为的是保证对不同类型不同个数的实参都能完成正确的传递
emit(type, ...args) {
// 发布事件
if (this.event[type]) {
this.event[type].forEach(cb => {
cb(...args)
});
} else {
return
}
}
Once方法
Once相比之前的可能会有点复杂,所以放最后了。这里我们定义一个新函数,名为fn,函数很简单,和前面的emit一样,都是通过...agrs去设置形参确保传参正确,然后把...args全被传给内部的cb直接调用,调用完成随后off掉,确保cb在回调函数的数组中被调用之后就没了。
once(type, cb) {
// 只订阅一次
const fn = (...args) => {
cb(...args)
this.off(type, fn)
}
this.on(type, fn)
}
最开始这里面的参数传递属实给我搞蒙了,其实很好捋清楚。在once里面,被添加到数组的是fn而不是cb。当emit触发的时候,执行的就是fn函数,前面写emit函数的时候也写清楚了 ,emit会把所有参数交给回调函数,这里就是交给了fn,然后fn再交给cb,执行完成后立即把cb通过之前写好的off函数删除。如此一来就能实现只订阅一次的效果了
通过发布订阅实现异步
讲完手写发布订阅,相信各位对如何通过发布订阅实现异步已经有了一个大概的思路。在html中,先new一个事件对象出来,然后在第一个函数中发布事件,随后给全局变量(在浏览器环境下就是window)添加订阅,把第二个函数作为回调函数传进去,就可以实现这一效果。
let newEvent = new Event('myAsync', { bubbles: true, cancelable: true })
function fnA(arams) {
setTimeout(() => {
console.log('A is ok');
window.dispatchEvent(newEvent)
}, 1000)
}
function fnB(arams) {
setTimeout(() => {
console.log('B is ok');
}, 500)
}
fnA()
window.addEventListener('myAsync', () => {
fnB()
})
总结
虽说通常情况下我们并不太会用发布订阅这种方法去实现异步处理,但是在一些组件库的封装中或者框架的搭建,发布订阅是必不可少的一门技术。希望本篇文章能够帮助到各位读者老爷,最后祝各位0 waring(s) 0 error(s)!
转载自:https://juejin.cn/post/7367335167394840615