发布订阅模式的TS实现

简介

发布订阅模式是一种常用的用于解耦的模式。

它和观察者模式的区别在于:

  • 观察者模式:被观察者需要维护一个观察者的集合;
  • 发布订阅模式:通信双方互相不知道对方的存在,通过第三方事件总线进行通信。

发布订阅模式在前端领域很常见,例如:

  • Vue 框架中组件的$on$emit方法;
  • Node.js 中 EventEmitter 中的 onemit 方法。

图示

  1. 订阅者通过on方法注册事件:

    image-20240817114351978

  2. 发布者通过emit触发回调列表:

    image-20240817114550701

    发布者和订阅者双方不知道各自的存在,它们仅通过Event Bus进行通信。

实现

事件总线最基本的两个方法是 onemit

常见的设计还有两个方法是 offonceoff 用于注销事件,onceon 的特例,表示仅订阅一次。

函数签名

type CallbackFn = (...args: any[]) => void;

on(eventName: string, callback: CallbackFn): void;
emit(enentName: string, ...args: any): void;
off(eventName: string, callback: CallbackFn): void;
once(eventName: string, callback: CallbackFn): void;

实现思路

在事件总线中需要建立起事件名回调集合的映射:

  • on时,将回调添加到指定事件名的回调集合中;
  • emit时,遍历指定时间名的回调集合,依次执行其中的回调函数;

回调集合可以使用Set实现,也可以使用数组实现。

由于on的实现需要做去重,建议使用Set,比较方便。

once的实现:

once 可以基于 on 和 off 实现,先使用 on 注册,执行回调之后就执行 off 注销,从而实现仅触发一次。

代码

使用 TypeScript 实现。

首先声明一个回调函数的类型,简化后续代码:

type CallbackFn = (...args: any[]) => void;

然后是声明 EventBus 类,成员属性中使用对象建立起 “事件名与回调集合” 的映射关系:

class EventBus{
    events: Record<string, Set<CallbackFn>> = {};

    constructor(){}
	
    on(eventName: string, callback: CallbackFn){ /* ... */ }
    emit(eventName: string, ...args: any[]){/* ... */}
    off(eventName: string, callback: CallbackFn){/* ... */}
    once(eventName: string, callback: CallbackFn){/* ... */}
}

on

on(eventName: string, callback: CallbackFn){
    if(!this.events[eventName]){
        this.events[eventName] = new Set();
    }
    this.events[eventName].add(callback);
}
  • 如果事件名不存在,则要初始化创建一个 Set 用于记录。
  • 如果事件名存在,则直接将回调添加到 Set 中。

这段代码可以通过 短路运算符 简化:

on(eventName: string, callback: CallbackFn){
    (this.events[eventName] ??= new Set()).add(callback);
}

emit

遍历回调集合就🆗了。(记得带上函数参数)

emit(eventName: string, ...args: any[]){
    this.events[eventName]?.forEach(cb => cb(...args));
}

off

注销事件也很简单,使用 Set 的 delete 方法就可以了。

off(eventName: string, callback: CallbackFn){
    this.events[eventName]?.delete(callback);
}

once

对 once 传入的回调函数做一层包装,在执行之后调用 off 注销事件。

使用箭头函数是为了 this 指向正确。使用 function 和 bind 也🆗,箭头函数比较简洁。

once(eventName: string, callback: CallbackFn){
    const handler = (...args: any[]) => {
        callback(...args);
        this.off(eventName, handler);
    }

    this.on(eventName, handler);
}

代码汇总

type CallbackFn = (...args: any[]) => void;

class EventBus{
  events: Record<string, Set<CallbackFn>> = {};

  constructor(){}

  on(eventName: string, callback: CallbackFn){
    (this.events[eventName] ??= new Set()).add(callback);
  }

  emit(eventName: string, ...args: any[]){
    this.events[eventName]?.forEach(cb => cb(...args));
  }

  off(eventName: string, callback: CallbackFn){
    this.events[eventName]?.delete(callback);
  }

  once(eventName: string, callback: CallbackFn){
    const handler = (...args: any[]) => {
      callback(...args);
      this.off(eventName, handler);
    }

    this.on(eventName, handler);
  }
}
posted @ 2024-08-17 12:35  feixianxing  阅读(49)  评论(0编辑  收藏  举报