手写发布订阅

400 阅读3分钟

发布订阅模式是一种常见的设计模式,通常用于前端开发中。该模式基于一种事件驱动的机制,其中一个对象(通常为发布者)可以发布事件,而其他对象(通常称为订阅者)则可以订阅该事件,以便在该事件发生时进行相应的操作。

在前端开发中,发布订阅模式通常用于解耦和组件通信。例如,一个组件可以发布一个事件,其他组件则可以订阅该事件,以便在事件发生时更新其状态或执行其他操作。这使得代码更具有可维护性和可扩展性,因为每个组件都可以独立地处理自己的逻辑,而不必与其他组件直接交互。

简单来说,发布订阅通常包含三个主要功能:监听事件(订阅事件 on)、取消监听事件(取消订阅 off)和触发事件(发布事件 emit)

事件驱动机制:通常指的是一种基于事件的编程模式,即通过触发事件来驱动代码的执行。例如通过监听 DOM 事件来响应鼠标点击、键盘输入等操作,或者通过监听 HTTP 请求的返回结果来更新页面内容。
解耦:通常指的是将不同组件之间的依赖降到最低,以减少代码之间的耦合度。
组件通信:指的是组件之间的信息传递,包括数据传递、状态同步、事件触发等。

普通实现

  1. 创建一个eventHub对象并实现发布订阅的三个方法,然后调用。
    const eventHub = {
        on: ()=>{},
        emit: ()=>{},
        off: ()=>{}
    }
    eventHub.on('click', f1)
    eventHub.off('click', f1)
    setTimeout(()=>{
        eventHub.emit('click', 'data')
    }, 3000)
    
  2. 当有多个对象订阅时应该怎么处理?如:
    eventHub.on('click', f1)
    eventHub.on('click', f2)
    我们可以通过队列实现先入先出,并通过map实现映射。
    所以我们定义一个map={}作为任务队列。
  3. 实现on方法:根据调用得知接受事件名和函数两个参数(name, fn)。
    然后执行入队操作:
    初始化:eventHub.map[name] = eventHub.map[name] || []
    入队:eventHub.map[name].push(fn)
  4. 实现off方法:
    判断eventHub.map[name]是否存在,不存在则return。
    eventHub.map[name]存在则判断是否存在fn。
    若fn存在则使其出队。
  5. 实现emit方法:
    判断任务队列中是否有相对应的事件,没有则返回。反之则通过 map 方法找到每一个函数,并一一执行,最后 return undefined(因为一般不关心返回值)。
// 最终代码实现
const eventHub = {
    map:{
        // click:[f1, f2]
    },
    on: (name, fn)=>{
        eventHub.map[name] = eventHub.map[name] || []
        eventHub.map[name].push(fn)
    },
    emit: (name, data)=>{
        const q = eventHub.map[name]
        if(!q){return}
        q.map(f = f.call(undefined, data))
        return undefined
    },
    off: (name, fn)=>{
        const q = eventHub.map[name] //alias 设计模式
        if(!q){return}
        const index = q.indexOf(fn)
        if(index < 0){return}
        q.splice(index,1)
    }
}
eventHub.on('click', f1)
eventHub.off('click', f1)
setTimeout(()=>{
    eventHub.emit('click', 'data')
}, 3000)

类实现

// 思路与上面基本一致。
class EventHub {
  map = {}
  on(name, fn) {
    this.map[name] = this.map[name] || []
    this.map[name].push(fn)
  }
  emit(name, data) {
    const fnList = this.map[name] || []
    fnList.forEach(fn => fn.call(undefined, data))
  }
  off(name, fn) {
    const fnList = this.map[name] || []
    const index = fnList.indexOf(fn)
    if(index < 0) return
    fnList.splice(index, 1)
  }
}
// 使用
const e = new EventHub()
e.on('click', (name)=>{
  console.log('hi '+ name)
})
e.on('click', (name)=>{
  console.log('hello '+ name)
})
setTimeout(()=>{
  e.emit('click', 'data')
},3000)