zhangdizhangdi

观察者 & 发布订阅模式 🔥

定义

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。

应用场景

  • DOM 事件
  • 异步回调函数(定时器,promise.then)
  • 组件生命周期
  • Vue 的响应式
  • Vue $emit watch

实现

观察者模式

js
class Subject {
  constructor(name) {
    this.name = name
    this.observers = []
  }
  add(observer) {
    this.observers.push(observer)
  }
  remove(observer) {
    this.observers = this.observers.filter(item => item !== observer)
  }
  notify(str) {
    this.observers.forEach(item => {
      item.update(`${this.name} ${str}`)
    })
  }
}

class Observer {
  constructor(name) {
    this.name = name
  }
  update(str) {
    console.log(`${this.name} 接收到 ${str}`)
  }
}

const sub = new Subject('目标1')
const observer1 = new Observer('观察者1-1')
const observer2 = new Observer('观察者1-2')
sub.add(observer1)
sub.add(observer2)
sub.notify('更新了')
sub.remove(observer2)
sub.notify('删除了一个watcher')

const sub2 = new Subject('目标2')
const observer3 = new Observer('观察者2-1')
const observer4 = new Observer('观察者2-2')
sub2.add(observer3)
sub2.add(observer4)
sub2.notify('更新了')
执行结果
观察者1-1 接收到 目标1 更新了
观察者1-2 接收到 目标1 更新了
观察者1-1 接收到 目标1 删除了一个watcher
观察者2-1 接收到 目标2 更新了
观察者2-2 接收到 目标2 更新了

发布订阅模式

js
class EventBus {
  constructor() {
    this.events = {
      /**
       * 'click':[fn1,fn2...],
       * 'scroll':[fn3,fn4...]
       */
    }
    this.onceEvents = {}
  }
  on(eventName, fn) {
    if (!this.events[eventName]) {
      this.events[eventName] = []
    }
    this.events[eventName].push(fn)
  }
  once(eventName, fn) {
    if (!this.onceEvents[eventName]) {
      this.onceEvents[eventName] = []
    }
    this.onceEvents[eventName].push(fn)
  }
  emit(eventName, ...args) {
    const targetEvents = this.events[eventName]
    const targetOnceEvents = this.onceEvents[eventName]
    if (!targetEvents && !targetOnceEvents) {
      console.log('!!!没有订阅事件可触发')
      return
    }

    if (targetEvents) {
      targetEvents.forEach(fn => {
        fn(...args)
      })
    }

    if (targetOnceEvents) {
      targetOnceEvents.forEach(fn => {
        fn(...args)
      })
      //once 执行一次就删除
      delete this.onceEvents[eventName]
    }
  }
  off(eventName, fn) {
    if (fn) {
      //删除指定事件
      if (this.events[eventName]) {
        this.events[eventName] = this.events[eventName].filter(
          curFn => curFn !== fn,
        )
      }
      if (this.onceEvents[eventName]) {
        this.onceEvents[eventName] = this.onceEvents[eventName].filter(
          curFn => curFn !== fn,
        )
      }
    } else {
      //删除全部
      if (this.events[eventName]) {
        delete this.events[eventName]
      }
      if (this.onceEvents[eventName]) {
        delete this.onceEvents[eventName]
      }
    }
  }
}

const eventBus = new EventBus()

//第一种事件
const clickFn1 = res => console.log(`${res} 1`)
eventBus.on('click', clickFn1)

const clickFn2 = res => console.log(`${res} 2`)
eventBus.on('click', clickFn2)

const clickFn3 = res => console.log(`${res} once 事件 3`)
eventBus.once('click', clickFn3)

eventBus.emit('click', '触发了点击')
console.log('############')

//第二种事件
const scrollFn1 = res => console.log(`${res} 1`)
eventBus.on('scroll', scrollFn1)

const scrollFn2 = res => console.log(`${res} once 事件 2`)
eventBus.once('scroll', scrollFn2)

eventBus.emit('scroll', '触发了scroll')
console.log('############')

//删除事件
eventBus.off('click', clickFn2)
eventBus.emit('click', '触发了点击')
console.log('############')

//删除事件
eventBus.off('scroll')
eventBus.emit('scroll', '触发了scroll')
执行结果
触发了点击 1
触发了点击 2
触发了点击 once 事件 3
############
触发了scroll 1
触发了scroll once 事件 2
############
触发了点击 1
############
!!!没有订阅事件可触发

参考