JavaScript设计模式:发布-订阅模式与观察者模式

228 阅读4分钟

本文将介绍发布-订阅模式与观察者模式,他们都属于行为型设计模式。下面我们先来看看“发布-订阅模式”。

发布-订阅模式

它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

现实中的发布-订阅模式

举一个生活中的例子,我们通过微信对某个感兴趣的公众号进行了订阅,公众号更新了文章,微信会给我们推送更新的消息。这便是现实生活中的发布-订阅模式,我们不用时时刻刻打开微信看这个公众号更新了没有。

DOM 事件

在开发中,我们常给 DOM 绑定事件监听函数,当事件触发时我们绑定的回调函数被调用。

document.body.addEventListener(
  "click",
  () => {
    console.log("点击事件触发");
  },
  false
);

由于我们无法预知用户在什么时刻点击,就需要监控用户点击document.body的动作。订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息,这是我们在开发中接触到的一种“发布-订阅模式”。这很像公众号订阅的例子,订阅者不知道什么时候公众号什么时候更新内容,于是在订阅后等待微信推送公众号更新的内容。

发布-订阅模式的通用实现

代码如下:

class EventEmitter {
  handlers = [];
  addListener(eventType, callback) {
    if (!this.handlers[eventType]) {
      this.handlers[eventType] = [];
    }
    this.handlers[eventType].push(callback);
  }
  removeListener(eventType, callback) {
    if (!this.handlers[eventType]) {
      return;
    }
    const idx = this.handlers[eventType].findIndex((i) => i === callback);
    this.handlers[eventType].splice(idx, 1);
  }
  emit() {
    const eventType = Array.prototype.shift.call(arguments);
    const callbacks = this.handlers[eventType];

    if (!callbacks || callbacks.length === 0) {
      return false;
    }

    for (let i = 0, cb; (cb = callbacks[i++]); ) {
      cb.apply(this, arguments);
    }
  }
  once(eventType, callback) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    function wrapper() {
      callback.apply(this, arguments);
      this.removeListener(eventType, wrapper);
    }
    this.addListener(eventType, wrapper);
  }
  removeAllListener(eventType) {
    if (!this.handlers[eventType]) {
      this.handlers[eventType] = [];
    }
  }
}

调用示例:

const eventEmitter = new EventEmitter();

const cb = (val) => {
  console.log("val * 10", val * 10);
};

eventEmitter.addListener("update", cb);

eventEmitter.emit("update", 10);

eventEmitter.removeListener("update", cb);

eventEmitter.emit("update", 20);

观察者模式

在我们的开发工作中,常有这样的情况。突然被领导拉去参加需求评审会,评审会后产品经理根据最终的评审结果修改需求文档,研发们也都知道了有个开发任务要开始了,等着出最新的需求文档后干活。几天后产品经理拉了个群,将需求文档发到群里并艾特相关的研发,研发收到文件后开始干活。这种发布者直接触及到订阅者的操作,叫观察者模式。

但如果产品经理没有拉群,而是把需求文档上传到了公司的需求平台上,需求平台感知到文件的变化、邮件通知了每一位订阅了该需求的开发者,这种发布者不直接触及到订阅者、而是由第三方来完成实际的通信的模式,叫做发布-订阅模式。

经过对比不难看出两种模式的区别在于是否存在第三方,由第三方完成实际通信。可以通过下图来直观的感受一下:

观察者模式与发布订阅.jpg

观察者模式的实现

从上面举的例子中,可以看出观察者模式最重要的两个角色是发布者(例子中的产品经理)和订阅者(研发)。发布者有着管理订阅者以及通知订阅者的能力,订阅者则需要具备接收通知的能力。

我们来实现发布者(Publisher)类:

class Publisher {
  constructor() {
    // 订阅者列表
    this.observers = [];
  }
  // 增加订阅者
  add(observer) {
    this.observers.push(observer);
  }
  // 移除订阅者
  remove(observer) {
    this.observers.forEach((item, idx) => {
      if (item === observer) {
        this.observers.splice(idx, 1);
      }
    });
  }
  // 通知所有订阅者
  notify() {
    this.observers.forEach((observer) => {
      observer.update(this);
    });
  }
}

再来实现订阅者(Observer)类:

class Observer {
  // update 方法用来接收通知
  update(publisher) {}
}

至此,我们完成了通用的发布者和订阅者类的设计。面对不同的业务场景,我们可以基于这两个类拓展,去完成复杂功能的开发。下面我们基于这两个类来拓展出上文中提到的开发经理的例子。

我们首先来实现开发经理的类:

class PMPublisher extends Publisher {
  constructor() {
    super();
    // 初始化需求文档
    this.prdInfo = null;
  }

  // 获取当前的需求文档的方法
  getPrdInfo() {
    return this.prdInfo;
  }

  // 更新需求文档
  setPrdInfo(info) {
    this.prdInfo = info;
    // 需求文档更新,通知所有群成员
    this.notify();
  }
}

再实现研发的类:

class DeveloperObserver extends Observer {
  constructor(name) {
    super();
    this.prdInfo = null;
    this.name = name;
  }

  // 覆写父类的 update 方法
  update(publisher) {
    // 获取最新的需求文档
    this.prdInfo = publisher.getPrdInfo();
    console.log(`${this.name}收到文档`, this.prdInfo);
    // 开始搬砖
    this.work();
  }

  // 具体的工作
  work() {
    console.log("开始后疫情时代背景下的工作...");
  }
}

最后是整个流程串起来:

const bugTerminator = new DeveloperObserver("BUG终结者");

const pm = new PMPublisher();
// 需求文档
const prd = { name: "需求XXX" };
// pm 开始拉群,把“BUG终结者”拉进群
pm.add(bugTerminator);
// pm 发送需求文档,并艾特了所有人
pm.setPrdInfo(prd);

以上,就是观察者模式的代码实现了。

参考文章,感兴趣的小伙伴可以看看:

纸质书籍:JavaScript 设计模式与开发实践

掘金小册:JavaScript 设计模式核心原理与应用实践