再手写一次发布订阅和观察者

647 阅读4分钟

发布订阅模式和观察者模式是开发中常用的设计模式和思想,利用它们可以做到数据更高级的通信,当然在Vue和React等框架中,也用到了它们,本篇就来说一下它们的实现原理并手写代码。

发布订阅模式

原理

在软件架构中,发布-订阅 是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。 —— 维基百科

发布订阅模式从它的概念里就可以看出来特点:发布者 发出的消息,不会发送给特定的 订阅者订阅者 不会直接接收发布者的消息,反过来也是,这意味着发布者和订阅者不知道彼此的存在。 那他们之间是怎么通信的呢?

原来在它们中间存在一个“第三者”,它被称之为 调度中心事件通道,它维持着 发布者订阅者 之间的联系,过滤所有 发布者 传入的消息并相应地分发它们给 订阅者

所以现在我们知道了,完成发布订阅的整个流程需要三个角色:

  1. 发布者
  2. 调度中心
  3. 订阅者

实现

在JS中,它们之间的逻辑是这样的,订阅者调度中心 订阅指定的事件,发布者调度中心 发布指定的事件,调度中心 通知 订阅者订阅者 收到消息,当然一个发布者事件可能会有多个 订阅者

从这个逻辑里,我们可以列出如下代码:

class EventEmitter{
  constructor(){
    // 汇总所有的事件和监听
    this.listeners = {};
  }
  
  /** 绑定事件的监听者
   * @param {String} eventType 事件类型
   * @param {Function} cb 回调函数
   */
  on(eventType, cb){
    // 如果还没有监听者就先初始化一下
    if(!this.listeners[eventType]){
      	this.listeners[eventType] = [];
    }
    // 塞入订阅者的回调
    this.listeners[eventType].push(cb);
  }
  
  /** 发布事件
   * @param {String} eventType 事件类型
   * @param {Function} args 参数列表,把emit传递的参数赋给回调函数
   */
  emit(eventType, ...args){
    // 如果已经订阅了事件,就执行
    if(this.listeners[eventType]){
      this.listeners[eventType].forEach(cb => {
        cb(...args)
      })
    }
  }
  
  /** 解绑事件的监听者
   * @param {String} eventType 事件类型
   * @param {Function} cb 回调函数
   */
  off(eventType, cb){
    // 如果当前事件存在监听者,就移除它
    if(this.listeners[eventType]){
      const index = this.listeners[eventType].findIndex(fn => fn == cb);
      if(index !== -1){
        this.listeners[eventType].splice(index, 1);
      }
      if(!this.listeners[eventType].length){
        // 如果没有事件监听它了,就直接删除这个事件类型
        delete this.listeners[eventType];
      }
    }
  }
}

这样的话,一个简单的发布订阅就实现了,我们就可以这样使用它:

// 实例化一个发布订阅
const ee = new EventEmitter();
// 注册一个监听者
ee.on("speak", function(){
  console.log("我讲话了!");
});
ee.emit("speak");
ee.on("speak", function(msg){
  console.log(`我说,${msg}`);
});
ee.emit("speak","你在干啥?");

// output: 
// 我讲话了!
// 我讲话了!
// 我说,你在干啥?

上面打印两次 “我讲话了” 是因为总共注册了2个 “speak” 的监听者,这样一个简易的发布订阅就成功啦!

观察者模式

原理

观察者模式发布订阅模式 不同,观察者模式 是没有 调度中心 的存在的,它是直接监听的对象,当一个对象的状态发生变化时,所有依赖于它的对象都将得到通知,并自动更新,它也是一种一对多的关系。

实现

那没有 调度中心 也就意味着一个对象被直接监听了,此时又得保证在移除的时候可以找到特定的监听者,所以在观察者和被观察者的定义里都需要一个类似唯一id的标识符,我们来下一下它的逻辑:

let obser_ids=0;
let obsed_ids=0;

// 观察者
class Observer {
  constructor(){
    this.id = obser_ids++;
  }
  
  // 数据发生变化后的回调
  update(...args){
    console.log(...args)
  }
}

// 被观察者
class Observed {
  constructor(){
    this.observers = [];
    this.id = obsed_ids++;
  }
  
  // 添加观察者
  addObserver(observer){
    this.observers.push(observer);
  }
  
  // 通知所有观察者
  notify(...args){
    this.observers.forEach(observer => {
      observer.update(...args);
    });
  }
  
  //移除观察者
  deleteObserver(observer){
    this.observers = this.observers.filter(o => {
      return o.id != observer.id;
    });
  }
}

观察到变化之后,遍历观察者数组执行回调函数,删除观察者通过唯一标识符判定进行删除,一个简单的观察者就实现了,我们可以测试一下:

// 实例化一个被观察者
let od = new Observed();
// 实例化两个观察者
let or1 = new Observer();
let or2 = new Observer();
// or1 和 or2 观察 od
od.addObserver(or1);
od.addObserver(or2);
// 通知所有观察者
od.notify("通知了!");

// output: 
// 通知了!
// 通知了!

可见两个观察者都检测到了被观察者的变化,例子成功!

我的公众号:道道里的前端栈,分享前端知识,嚼碎的感觉真奇妙~