面试官:请手写一个发布订阅模式

686 阅读4分钟

前言:

发布订阅模式是一种软件设计模式,其中发布者负责发布消息,而订阅者订阅自己感兴趣的消息,当发布者发布消息时,所有订阅该消息的订阅者都会收到通知并执行相应的操作,实现了消息的解耦和系统组件之间的松散耦合。

发布订阅模式可解耦消息发布与订阅,实现组件松散耦合。

发布订阅模式用于解耦消息,组件耦合松散。

它是一种消息范式,涉及消息的发送者(称为发布者)和接收者(称为订阅者)。在这种模式中,发布者和订阅者不直接相互了解,而是通过一个称为"事件通道"或"消息代理"的中间人来管理消息的分发。

开始手写

准备容器

constructor() {
    this.cache = {}
  }

使用了es6 class的方式去完成本次手写,每当我们去实例一个对象时,都会调用constructor()

收集订阅事件

on(name, fn) {
    if (this.cache[name]) {
      this.cache[name].push(fn)
    } else {
      this.cache[name] = [fn]
    }
  }

对于on方法,我们可以简单地以视频平台的博主与粉丝之间的联系来理解。

对于博主来说,想要接受到他发布的动态或者视频,用户首先得订阅他的账号,这样在每次博主更新自己的内容时,用户都能够收到信息。

同样的对于本段代码来说,name是指博主的账号,而fn可以抽象地理解为用户账号,由于一个博主可以被多个用户关注订阅,所以就解释了name对于的fn为什么是一个数组。

而当不存在name的值时,首先对他进行一个初始化。

执行事件

emit(name, once = false, ...args) {
    if (this.cache[name]) {
      // 执行 但不要影响订阅者
      let tasks = this.cache[name].slice()
      for (let fn of tasks) {
        fn(...args)
      }
      if (once) {
        delete this.cache[name]
      }
    }
  }

对于emit方法同样的我们以视频平台的例子来理解。

每当博主发布了自己的动态或者视频时,需要对每一个订阅了他账号的用户推送自己的内容

因此,首先判断容器(cache)中是否存在博主账号,存在时利用silce实现一个浅拷贝的效果,不影响容器

直接遍历所有的粉丝(fn),传入对应的参数,推送自己的信息(执行函数)

额外还有个可选的参数,once,如果传入的值为true,表示只对当前账号执行一次操作,并且在操作完成后对账号进行删除。

移除事件

off(name, fn) {
    let tasks = this.cache[name]
    if (tasks) {
      const index = tasks.findIndex(f => f === fn || f.callback === fn)
      if (index >= 0) {
        tasks.splice(index, 1)
      }
    }
  }

对于用户来说有了关注,自然也有了取消关注的选项

但在博主的视角来看,我们需要从所有的粉丝列表当中找出,想要取关的账号,然后再将这个账号移除,以便在之后推送自己内容时,不会再推送到取消关注的用户。

在代码层面来看,先拿到整个粉丝列表,使用迭代器findIndex直接快速找到,取关的账号(fn),由于findIndex在查找不到时,会返回-1,所以当index的值大于等于0时,表示找到了该粉丝(fn),直接使用splice影响原数组删除。

订阅一次

once(name, fn) {
    // 创建一个新的回调函数,该函数会在执行后自动取消订阅
    const wrappedFn = (...args) => {
      this.off(name, wrappedFn);
      fn(...args);
    };

    this.on(name, wrappedFn);
  }

有的时候,即使用户没有订阅博主但却依旧可以浏览博主发布的内容,由于必须要先订阅才能看到博主的内容。这时,我们可以直接先造出一个虚假的关注博主的粉丝,以他为跳板浏览博主发布的信息。

这个虚假的粉丝就是wrappedFn,在用户执行完毕后直接将其off取消订阅,就实现了只订阅一次的功能。

整体代码

//EventEmitter
class EventEmitter {
  constructor() {
    this.cache = {}
  }
  //收集订阅
  on(name, fn) {
    if (this.cache[name]) {
      this.cache[name].push(fn)
    } else {
      this.cache[name] = [fn]
    }
  }
  //执行事件
  emit(name, once = false, ...args) {
    if (this.cache[name]) {
      // 执行 但不要影响订阅者
      let tasks = this.cache[name].slice()
      for (let fn of tasks) {
        fn(...args)
      }
      if (once) {
        delete this.cache[name]
      }
    }
  }
  //移除订阅
  off(name, fn) {
    let tasks = this.cache[name]
    if (tasks) {
      const index = tasks.findIndex(f => f === fn || f.callback === fn)
      if (index >= 0) {
        tasks.splice(index, 1)
      }
    }
  }
  //执行一次
  once(name, fn) {
    // 创建一个新的回调函数,该函数会在执行后自动取消订阅
    const wrappedFn = (...args) => {
      this.off(name, wrappedFn);
      fn(...args);
    };

    this.on(name, wrappedFn);
  }
}

总结

发布-订阅模式在前端开发中有许多实际应用场景,它提供了一种松散耦合的机制,允许不同模块或组件之间进行灵活的通信(例如vue2中的事件总线通信,vue3的响应式实现等)。