手写一个发布订阅

502 阅读7分钟

前言

发布订阅是vue源码的核心思想,在面试中也很容易被面试官问到,甚至让你手搓一个发布订阅,待会小编就带大家来手搓一个发布订阅,在这之前,咱们先来一个情景带你理解一下发布订阅:假设我们想要买房,由于当前的一期房子已经售空,售楼部就让我们去关注他们的公众号,二期在建,一旦建好就会在公众号及时发布消息。我们关注公众号这个行为就是订阅一个事件,公众号发消息就是发布一个事件,并且一个公众号一般会有很多人去订阅。

自定义事件

在认识发布订阅模式之前我们需要先认识下自定义事件,面试官有时候就会请你聊聊什么是自定义事件。

我们应该清楚js一些内置的事件,比如点击事件,鼠标事件,键盘事件,焦点事件,滚动事件等。其实事件的本质就是模块对象之间的信息通信

Event构造函数

Event()构造函数创建一个事件(支持冒泡不可取消

咱们可以看到官方文档里面解释的参数Event() - Web API 接口参考 | MDN (mozilla.org)

image-20240903112549331

let ev = new Event('look', { bubbles: true, cancelable: true })

创建完了这个事件就需要有人去订阅这个事件,addEventListener就是订阅事件。这里写法为了刻意体现冒泡和取消

box.addEventListener("look", (e) => { 
    if (event.cancelable) {
        event.preventDefault();  // 如果事件可以取消就取消事件
    } else {
        console.log("在box上触发了look事件")  
        }
}) 

window.addEventListener("look", () => {
            console.log("在window上触发了look事件")   // 不需要在window上发布,它会冒泡出来,false就不行 
        })

在box身上发布该自定义事件

box.dispatchEvent(ev);

像是js自带的事件就是默认发布在全局中,因此不需要我们去发布,所以这里的自定义事件不需要发布在全局身上,因为这是默认就有的,所以window本身就订阅了这个事件

正文

那么如何手搓一个发布订阅呢?咱们来看下面的开发

class EventEmitter {
	constructor() {
	
	}
	on() {  // 订阅
	
	}
	emit() {  // 发布
	
	}
	off() {  // 关闭
	
	}
	once() {  // 订阅一次
	
	}
}

这里咱们用的是es6里的class的语法。

咱们先去完成onemit。on就是去订阅,emit就是去发布。我们先写下将会如何使用,先创建一个事件,这里既然手搓我肯定用到构造函数去创建一个Event

思路:

on的职责是触发事件但是它的触发条件是emit了才触发,既然这样使用,那么on的形参就是(事件,回调),接下来就要判断事件是否存在,也就是说emit发布了才会有这个事件,emit执行了才会触发on的回调

首先要判断是否有这个事件存在,也就是在constructor中定义全局变量,去放置一个对象去存事件。

class Event{
    constructor(){
        this.enent={
            //'houseSource':[buy,buy2]
            //'carPlace':[buyCarPlace]
        }
        this.onceFn=[]
    }
}

on

看这个事件是否存在,如果不存在,我们就要存入到event对象中去,人家都订阅了你就得存下来。这里我们将其存成一个数组的形式,因为一个事件可以被多个人订阅,多个订阅就有多个回调需要去执行。如果事件已经存在,我们就把事件加进去push。这个逻辑就是多个on('buy')对应相同数量的buy,满足同一事件被多人订阅

//订阅
    on(type,fn){
        if(!this.enent[type]){
            this.enent[type]=[]
        }
        this.enent[type].push(fn)
    }

emit

先看形参,第一个形参就是事件,第二个形参是回调的参数,由于你无法判断人家传几个参数,因此我们用上arguments类数组,去解构它,emit的作用就是触发回调,当然得是on了才会触发,on已经干了添加event对象这事情,因此就是只要对象event中有这个事件,我们就是挨个触发它,没有就直接return

//发布
emit(type, ...args) {
	if(!this.event[type]) {
		return 
	} else {
		this.event[type].forEach(fn => {
			fn(...args)   // 这里不打...接受的就是数组
		})
	}
}

好了,到这里你就可以看看on和emit的效果了,目前的代码就是这样的

class Event{
    constructor(){
        this.enent={
            //'houseSource':[buy,buy2]
            //'carPlace':[buyCarPlace]
        }
    }
    //订阅
    on(type,fn){
        if(!this.enent[type]){
            this.enent[type]=[]
        }
        this.enent[type].push(fn)
    }
    //发布
	emit(type, ...args) {
	if(!this.event[type]) {
		return 
	} else {
		this.event[type].forEach(fn => {
			fn(...args)   // 这里不打...接受的就是数组
		})
	}
}
	once(){
	}
	off(){
	
	}

const e=new Event()
function buy(msg){
    console.log('剑哥买房成功',msg)
}
function buy2(){
    console.log('老王买房成功2')
}
function buyCarPlace(){
    console.log('发哥买到了车位')
}

e.on('houseSource',buy)
//e.on('houseSource',buy2)
//e.on('carPlace',buyCarPlace)
//e.on('carPlace',buy)
//e.off('houseSource',buy)
// e.once('houseSource',buy)
// e.once('houseSource',buy)
//e.once('houseSource',buy)
//e.on('houseSource',buy)

//e.emit('houseSource','事件数据')
e.emit('houseSource','事件数据')
e.emit('carPlace')

可以实现多个人订阅相同事件,一发布,一对多的关系

e.on('houseSource',buy)
e.on('houseSource',buy)
e.emit('houseSource','事件数据')

image-20240903115157105

也可以实现多个对象订阅同一事件,执行各自的回调函数

e.on('houseSource',buy)
e.on('houseSource',buy2)
e.emit('houseSource','事件数据')

image-20240903115457921

也可以实现多个对象订阅不同的事件,执行各自的函数

e.on('houseSource',buy)
e.on('carPlace',buy2)
e.emit('houseSource','事件数据')
e.emit('carPlace'

image-20240903115719719

正常来讲这两个完成了,也就手搓完毕了,但是以防万一面试官威胁你,我们继续完成onceoff

先看下我们会如何实现它,我们订阅得再多,只要在emit之前off掉就无法实现打印

e.on('houseSource',buy)
e.on('houseSource',buy)
e.on('houseSource',buy)
e.off('houseSource',buy)
e.emit('houseSource','事件数据')

off

off的形参就是取消的事件,和哪个回调,依旧是先判断,如果事件本身就不存在,就不存在取消一说,否则就是有人订阅过,我们直接把这个回调从event对象中移除掉就可以,移除指定对象的指定位置,可以用filter过滤掉

//取消订阅
    off(type,fn){
        this.enent[type]=this.enent[type].filter((item)=>{
            return item!==fn
        })
    }

once

这里once订阅一次的使用场景就是订阅一次之后就无法订阅了,可能有两种意思,一种是发布多次我只订阅一次,就是只认第一次on,还有

可能理解为就是只能订阅一次。这里两种我都跟大家说下

先说第一种,once同样需要和on一样,但只认第一次on,所以订阅一次后取消掉就可以,取消就用off取消,我们可以拿on中的回调放到once里调用,然后取消即可,记得传参

once(type, fn) {
      
        const reWriteFn=()=>{//发布多次只执行一次
            fn()
            this.off(type,reWriteFn)
        }
        this.on(type,reWriteFn)
      }

第二种:

once(type, fn) {
         if (this.onceFn.includes(fn)) {//只能订阅一次
           return
        }
        this.onceFn.push(fn)
         this.on(type, fn) 
      }

好了,这就是全部的手搓发布订阅了,给大家一份全部代码

class Event{
    constructor(){
        this.enent={
            //'houseSource':[buy,buy2]
            //'carPlace':[buyCarPlace]
        }
        this.onceFn=[]
    }
    //订阅
    on(type,fn){
        if(!this.enent[type]){
            this.enent[type]=[]
        }
        this.enent[type].push(fn)
    }
    //发布
	emit(type, ...args) {
	if(!this.event[type]) {
		return 
	} else {
		this.event[type].forEach(fn => {
			fn(...args)   // 这里不打...接受的就是数组
		})
	}
}
    //取消订阅
    off(type,fn){
        this.enent[type]=this.enent[type].filter((item)=>{
            return item!==fn
        })
    }
    once(type, fn) {
        // if (this.onceFn.includes(fn)) {//只能订阅一次
        //   return
        // }
        // this.onceFn.push(fn)
        // this.on(type, fn)
    
        const reWriteFn=()=>{//发布多次只执行一次
            fn()
            this.off(type,reWriteFn)
        }
        this.on(type,reWriteFn)
      }
}
const e=new Event()
function buy(msg){
    console.log('剑哥买房成功',msg)
}
function buy2(){
    console.log('老王买房成功')
}
function buyCarPlace(){
    console.log('发哥买到了车位')
}


e.on('houseSource',buy)
e.on('houseSource',buy)
//e.on('carPlace',buyCarPlace)
//e.on('carPlace',buy2)
e.off('houseSource',buy)
// e.once('houseSource',buy)
// e.once('houseSource',buy)
//e.once('houseSource',buy)
//e.on('houseSource',buy)

//e.emit('houseSource','事件数据')
e.emit('houseSource','事件数据')
//e.emit('carPlace')

结尾

发布订阅在面试中还是可能会被问到的,所以我们还是需要学会手搓,

发布订阅的应用场景

面试官:如何不使用promise处理下面的异步,使其A先执行,B再执行

<script>
  function fnA() {
    setTimeout(() => {
      console.log('请求A完成')
    }, 1000)
  }

  function fnB() {
    setTimeout(() => {
      console.log('请求B完成')
    }, 500)
  }
</Script>

其实这里我们就可以用发布订阅的思想去处理这个问题,

这个方法处理异步虽然没得promise优雅,但是非常高级,阮一峰老师也指明可以这样处理异步。

没错!我们可以用发布订阅保证A先执行完再执行B,A调用完发布一个事件,让B去订阅这个事件。

就是直接在A函数体中发布一个事件,然后调用A,之后让B去订阅这个事件,肯定大家会疑惑了,B又不是个节点怎么去订阅,其实任何事件都是默认发布在window上的,所以我们用window去订阅,回调直接写B函数即可

<script>
  let ev = new Event('ahead')

  function fnA() {
    setTimeout(() => {
      console.log('请求A完成')
      window.dispatchEvent(ev)
    }, 1000)
  }
  
  fnA()   // 让A执行,也就是开始发布事件

  window.addEventListener('ahead', function fnB() {
    setTimeout(() => {
      console.log('请求B完成')
    }, 500)
  })
</Script>

image-20240903121546571

当然,我们不会这样去处理异步,都是用promise,或者说syncawait。当你给面试官讲这个方法去处理异步,面试官一定会惊叹!