初探发布订阅模式

354 阅读4分钟

介绍

发布-订阅模式, 定义对象间一对多的依赖关系,当一个对象状态发生改变,依赖于它的对象可以收到通知,并做出改变

  • 现实中的 比如你女盆友在某东的抢购中看到一只口红, 很心水, 灰常高兴的点进去,发现当前区域无货,但是又不想放弃,机智的她问询问客服什么时候到货,客服balabala一大堆,告诉可以在商品详情页面点击[到货通知],在货源充足时,会收到到货通知的信息,果不其然,在等待个把星期后,在快熄灯的时候收到了到货信息.

  • 在以上这个例子中, 你女盆友作为订阅者, 某东作为发布者,当货源到时,某东会取出订阅记录,通知所有订阅者,心心念的口红到了,可以剁手啦

  • 常见场景 vue中的数据绑定 vue的on和emit

  • 特点 广泛应用于异步编程中(替代了传递回调函数) 对象之间松散耦合的编写代码

自定义事件

定义一个Shop类,可以添加订阅者,在发布消息后,调用订阅者做出响应动作

  // 定义Shop类
  class Shop {
    constructor() {
      // 存储订阅者的回调函数
      this.subs = []
    }

    // 订阅
    on(fn) {
      // 添加订阅者的回调函数
      this.subs.push(fn)
    }
    // 发布信息
    emit() {
      // 取出订阅者的回调函数, 调用函数,完成通知
      this.subs.forEach(sub => {
        sub.apply(this, arguments)
      })
    }
  }

使用如下

  // 实例化Shop
  const shop = new Shop()

  // 订阅者订阅消息
  shop.on((day, price) => {
    console.log(`您期待天到货${day}, 期待价格是${price}`)
  })

  shop.on((num) => {
    console.log(`您期数量大于${num}`)
  })

  setTimeout(() => {
    shop.emit(30, 998)
    shop.emit('端茶和倒水', '足球')
  }, 3000)

结果如下:

  您期待天到货30, 期待价格是998
  您期数量大于30
  您期待天到货20, 期待价格是undefined
  您期数量大于20

上述代码存在一个问题, 任意一个消息发出(有的订阅者只希望在指定价格收到通知,有的希望数量大于指定量收到通知), 所有订阅者都会收到消息, 体验不太好, 订阅者只关心想要关心的内容

改进

怎么样只接受自己想要的信息,需要不同消息进行分类,在订阅的时候,告诉对方消息的类型,在触发的时候,也告诉消息和详细的类型

修改存储订阅者的容器subs, 改用对象, 使得可以存储类型, 类型的值对应存储自己的订阅者 在订阅时,根据订阅的类型,存储订阅者的函数 发布消息时, 取出类型, 调用该类型下的订阅者函数

  class Shop {
    constructor() {
      // 存储订阅者的回调函数, 使用对象进行存储, 区分不同类型的订阅 { 'moreThan': [fn, fn2] }
      this.subs = {}
    }

    // 订阅
    on(type, fn) {
      // 如果之前没有该类型的订阅,新加一个
      if (!this.subs[type]) {
        this.subs[type] = []
      }

      // 添加订阅者的函数
      this.subs[type].push(fn)
    }
    // 发布信息
    emit() {
      // 取出类型, 类型在arguments对象的第一个, 取出第一个后, 后面的就是真正的参数
      const type = [].shift.call(arguments)
      // 依次调用该类型下的所有函数
      const fnList = this.subs[type]
      if(fnList && fnList.length > 0) {
        // 取出订阅者,发出通知
        fnList.forEach(sub => {
          sub.apply(this, arguments)
        })
      }
    }
  }

使用如下:

  const shop = new Shop()

  // 订阅者订阅消息
  shop.on('atDayAndLessPrice', (day, price) => {
    console.log(`您期待天到货${day}, 期待价格是${price}`)
  })

  shop.on('moreThan', (num) => {
    console.log(`您期数量大于${num}`)
  })

  setTimeout(() => {
    shop.emit('atDayAndLessPrice', 30, 998)
    shop.emit('moreThan', 20)
    shop.emit('moreThan', 30)
  }, 3000)

结果如下:

  您期待天到货30, 期待价格是998
  您期数量大于20

移除订阅

上面的功能只有添加, 没有移除, 其实说白了,就是把订阅者的回调函数移除就行了,当订阅函数为空时,直接移除掉该类型的订阅

注意, 需要比对是不是同一个函数,所以订阅的时候不能使用匿名函数, 只能先定义回调函数,在订阅消息

  • 添加remove方法
  • 获取需要移除的订阅类型
  • 通过类型,获取该类型下的所有函数
  • 找到需要移除的函数,进行移除
  • 判断该类型下, 回调函数列表是否为空,如果为空,直接移除该类型
  // 定义Shop类
  class Shop {
    constructor() {
      // 存储订阅者, 使用对象进行存储, 区分不同类型的订阅 { 'moreThan': [fn, fn2] }
      this.subs = {}
    }

	// 省略...

    // 移除订阅
    remove(type, fn) {
      const fnList = this.subs[type]
      if(fnList && fnList.length > 0) {
        // 查找需要移除函数的索引
        const index = fnList.findIndex(v => v === fn)
        if(index !== -1) {
          // 移除
          this.subs[type].splice(index, 1)
        }

        // 该类型下回调函数全部被移除, 去掉该类型
        if(fnList.length === 0) {
          this.subs[type] = null
          delete this.subs[type]
        }
      }
    }
  }

使用示例:

需要定义具名函数,方便移除,否则不知道是移除那个函数

  const shop = new Shop()

  const dayPriceTest = (day, price) => {
    console.log(`您期待天到货${day}, 期待价格是${price}`)
  }
  const numTest = (num) => {
    console.log(`您期数量大于${num}`)
  }
  const numTest2 = (num) => {
    console.log(`那啥, 您期数量真的真的大于: ${num}`)
  }

  // 订阅者订阅消息
  shop.on('atDayAndLessPrice', dayPriceTest)
  shop.on('moreThan', numTest)
  shop.on('moreThan', numTest2)
  // shop.remove('atDayAndLessPrice', dayPriceTest)

  setTimeout(() => {
    shop.emit('atDayAndLessPrice', 30, 998)
    shop.emit('moreThan', 20)
  }, 3000)

结果如下:

您期待天到货30, 期待价格是998
03 发布-订阅.html:112 那啥, 您期数量真的真的大于: 20