发布订阅模式-EventEmitter

151 阅读2分钟

发布订阅

步骤

  1. 通过 button.addEventListener('click', fn) 监听/订阅事件
  2. 用户点击触发 button.dispatchEvent(...'click') 执行,即触发事件
  3. fn 调用

提问

  • 如何取消监听/订阅?- button.removeEventListener
  • 如果多次监听会怎样?- 按监听顺序依次执行 fn1、fn2
  • 如果将第 1、2 步颠倒过来会怎样?- 会使得 fn 不被执行
  • button 是怎么得到 addEventListener 方法的?- 通过原型链得到的
const button = document.getElementById('x')
const fn = () => console.log('hi')
//监听 click 事件
//fn 叫做事件处理函数
button.addEventListener('click', fn)

//取消监听
button.removeEventListener('click', fn)

setTimeout(() => {
  //button.click() 
  //触发事件
  //派发事件
  //发射事件
  button.dispatchEvent(new Event('click'))
}, 5000)

实现发布订阅

第一次尝试(不考虑事件名)

class EventEmitter {
  constructor() {
    this.queue = []
  }

  addEventListener(fn) {
    this.queue.push(fn)
  }
  removeEventListener(fn) {
    const index = this.queue.indexOf(fn)
    if (index < 0) return //找不到就不用删了
    this.queue.splice(index, 1)
  }
  dispatchEvent() {
    //调用时可能不止一个fn
    this.queue.forEach(fn => fn())
  }

}

const obj = new EventEmitter()
const fn = () => console.log('hi')

obj.addEventListener(fn)
// obj.removeEventListener(fn)

setTimeout(() => {
  obj.dispatchEvent()
}, 5000);

ajax()//会在未来调用api.dispatchEvent

第二次尝试(考虑事件名)

class EventEmitter {
  constructor() {
    this.queue = { /* xxx: [] 无法初始化 */ }
  }
  addEventListener(name, fn) {
    this.queue[name] = this.queue[name] || [] // 初始化
    this.queue[name].push(fn)
  }
  dispatchEvent(name) {
    // 可能直接执行派发事件,没有监听,可能没有初始化
    // 防御性编程
    // if (this.queue[name] === undefined) { return }
    this.queue[name]?.forEach(fn => fn())
  }
  removeEventListener(name, fn) {
    if (this.queue[name] === undefined) { return }
    const index = this.queue[name].indexOf(fn)
    if (index >= 0) {
    this.queue[name].splice(index, 1)
    }
  }
}

const obj = new EventEmitter()
const fn = () => console.log('hi')

obj.addEventListener('xxx', fn)
// obj.removeEventListener(fn)

setTimeout(() => {
  obj.dispatchEvent('xxx')
}, 5000);

第三次尝试(考虑fn的参数)

class EventEmitter {
  constructor() {
    this.queue = { /* xxx: [] 无法初始化 */ }
  }
  addEventListener(name, fn) {
    this.queue[name] = this.queue[name] || [] // 初始化
    this.queue[name].push(fn)
  }
  dispatchEvent(name, ...args) {
    this.queue[name]?.forEach(fn => fn.call(undefined, ...args))
  }
  removeEventListener(name, fn) {
    if (this.queue[name] === undefined) { return }
    const index = this.queue[name].indexOf(fn)
    if (index >= 0) {
      this.queue[name].splice(index, 1)
    }
  }
}

const obj = new EventEmitter()
const fn = (...args) => console.log(...args)

obj.addEventListener('xxx', fn)
// obj.removeEventListener(fn)

setTimeout(() => {
  obj.dispatchEvent('xxx', '饿死了', '烦死了')
}, 5000);

总结

发布订阅模式的特点

  • api 提供了 addEventListener / on / subscribe
  • api 提供了 dispatchEvent / emit / trigger
  • api 提供了 removeEventListener / off / unsubscribe
  • 满足上述条件的对象被称为 EventEmitter,实现了发布订阅模式

本质

  • 回调(函数) 放在 队列(数组) 里,等待被逐个调用

发布订阅解决了什么问题?

  1. 所有异步任务都可以用发布订阅来管理
  2. 先订阅成功事件和失败事件xhr.on('load'); xhr.on('error')
  3. 再在任务完成时触发成功或失败事件xhr.emit('load'); xhr.emit('error')
  4. 是一个通用的异步任务管理方案

缺点是什么?

事件少时还行;但当事件过多时,很难管理