typescript实现观察者模式&发布订阅模式

观察者模式

《Head First 设计模式》原文:

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

观察者模式是一种对象行为型模式。

要点

  1. 有观察者和被观察者
  2. 所有观察者一视同仁(所有观察者的回调函数,函数签名都相同)
  3. 将观察者添加到被观察者
  4. 遍历所有观察者,执行回调函数

思路

基于类实现

创建 2 个类 Observer:观察者,Subject:被观察者

Observer

有 1 个函数:update

函数 参数 作用
update 任意,根据业务逻辑来添加参数 回调函数,供 Subject 调用
Subject

有一个私有属性:list/set,用于保存所有已注册的观察者

有 3 个函数:add,remove,notify

函数 参数 作用
add 1 个,为 Observer 将观察者添加进 Subject 的观察者数组中
remove 1 个,为 Observer 将观察者从 Subject 的观察者数组中移除
notify 2 个:传给观察者的 update 的参数(可选参数),和 update 的 this 上下文(可选参数) 通知所有观察者,执行观察者的回调函数

基于函数实现

创建 1 个类

Subject:被观察者

有一个私有属性:Map<string,Function>,用于保存所有已注册的观察者,根据 key 来区分,相同 key 的观察者,后面注册的会覆盖前面的

其实可以用 Map<string,Array<Function>>来定义类型,将同一个事件的多个回调都保存下来,notify 时全部执行一遍。

函数 参数 作用
add 2 个:事件名,和事件回调函数 将观察者添加进 Subject 的观察者集合中
remove 1 个:事件名 将观察者从 Subject 的观察者集合中移除
notify 2 个:回调函数的参数(可选参数),和回调函数的 this 上下文(可选参数) 通知所有观察者,执行观察者的回调函数

实现

编程语言:typescript

基于类实现

typescript 复制代码
class Observer {
  constructor(/* sub?: Subject */) {
    // if (sub) {
    //   sub.add(this);
    // }
  }
  update(args: any[]) {
    console.log("param: ", args);
    console.log("this: ", this);
  }
}

class Subject {
  private set: Set<Observer> = new Set();

  add(obj: Observer) {
    this.set.add(obj);
  }
  remove(obj: Observer) {
    this.set.delete(obj);
  }
  notify(args: any, context?: any) {
    for (const obj of this.set) {
      // 考虑到update函数可能带有参数,update函数体内部可能用到了this,所以函数参数和this上下文需要从外部传入。这里用了apply来改变update的this指向
      obj.update.apply(context, args);
    }
  }
}

测试代码

typescript 复制代码
const observer = new Observer();
const observer2 = new Observer();
const subject = new Subject();
subject.add(observer);
subject.add(observer2);
subject.notify(["init"]);
subject.remove(observer2);
subject.notify(["after remove"], "fake this");

基于函数实现

typescript 复制代码
class FnSubject {
  private map: Map<string, Function> = new Map();

  add(event: string, fn: Function) {
    this.map.set(event, fn);
  }
  remove(event: string) {
    this.map.delete(event);
  }
  notify(args: any, context?: any) {
    for (const [key, value] of this.map.entries()) {
      // 考虑到回调函数可能带有参数,回调函数体内部可能用到了this,所以回调函数的参数和this上下文需要从外部传入。这里用了apply来改变回调函数的this指向
      value.apply(context, args);
    }
  }
}

测试代码

typescript 复制代码
const fnSub = new FnSubject();
fnSub.add("event1", (args: any[]) => {
  console.log("param: ", args);
  console.log("this: ", this);
});
fnSub.add("event1", (args: any[]) => {
  console.log("new param: ", args);
  console.log("new this: ", this);
});
fnSub.add("event2", function (args: any[]) {
  console.log("event2 param: ", args);
  console.log("event2 this: ", this);
});
fnSub.notify(["init"]);
fnSub.remove("event1");
fnSub.notify(["after remove"], "fake this");

缺点

一次性通知所有观察者的回调函数(广播),无法只通知一部分观察者(单播/多播)

所有观察者的函数参数类型,个数必须完全一致,不够灵活

js 中的使用场景

  • dom0 级事件:onerror,onmessage
  • Vue 响应式(Object.defineProperty 和 Proxy)

其他语言的实现

java 原生的实现:observer-observable 接口

发布订阅模式

要点

  1. 保存所有的事件和每个事件的所有回调函数(同一个事件,允许注册多个回调函数)
  2. 发布消息时,传入事件名和事件参数,根据事件名找到所有该事件的回调函数,逐个执行

思路

创建一个类:PubSub

有 1 个私有属性:Map<string, Array<Function>>,用于保存所有事件的回调

有 3 个函数

on,off,emit

函数 参数 作用
on 2 个:事件名,回调函数 添加订阅
off 1 个:事件名 取消订阅
emit 必选:事件名,可选:回调函数的参数和 this 上下文 发布消息

实现

编程语言:typescript

PubSub 类

typescript 复制代码
class PubSub {
  private map: Map<string, Array<Function>> = new Map();
  on(event: string, fn: Function) {
    const item = this.map.get(event);
    if (item) {
      item.push(fn);
    } else {
      this.map.set(event, [fn]);
    }
  }
  emit(event: string, payload: any[], context?: any) {
    const callbacks = this.map.get(event);
    if (callbacks) {
      callbacks.forEach((fn) => {
        fn.apply(context, payload);
      });
    }
  }
  off(event: string) {
    this.map.delete(event);
    console.log(`remove event: ${event}`);
  }
}

测试代码

typescript 复制代码
const pubsub = new PubSub();
pubsub.on("event1", (param1: string) => {
  console.log(`event1 params: ${param1}`);
});
pubsub.on("event1", function (param2: string) {
  console.log(`event1 callback params: ${param2}`);
  console.log(`this: ${this}`);
});
pubsub.on("event2", (param2: string) => {
  console.log(`event2 params: ${param2}`);
});
pubsub.emit("event1", ["event1 payload"]);
pubsub.emit("event1", ["event1 next payload"], "fake this");
pubsub.emit("event2", ["event2 payload"]);
pubsub.off("event1");
pubsub.emit("event2", ["after remove event1, event2 payload"]);

注意事项

先订阅再发布,否则回调函数永远不会执行

和观察者模式的区别

观察者模式中,Subject是知道Observer的,但是发布订阅模式中,发布者、订阅者彼此不知道对方的存在。

js 中的使用场景

  • dom2 级事件:addEventListener
  • EventEmitter
  • rxjs
  • vue 的 EventBus
相关推荐
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
Jacky(易小天)2 小时前
MongoDB比较查询操作符中英对照表及实例详解
数据库·mongodb·typescript·比较操作符
虾球xz2 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇2 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端
程序猿阿伟3 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒3 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript