手撕设计模式: 发布订阅模式基础实践

241 阅读3分钟

引言

发布订阅模式(Publish-Subscribe Pattern),也称为Pub-Sub模式,是一种软件架构模式,它定义了一种事件驱动的通信机制,允许多个对象(称为“订阅者”)以“订阅”的方式表达对特定事件的兴趣,并且当这些事件发生时,事件的“发布者”会自动将事件通知给所有已订阅的订阅者。

效果体验

实体关系图

Subscriber: 订阅者-对事件感兴趣的对象,它们注册自己到事件通道对象中以接收来自发布者的事件通知
Event Channel: 事件通道-管理订阅者列表并负责将接受发布者通知以调用订阅者的事件
Publisher: 发布者-创建事件并将其通知给事件通道对象

erDiagram
  "Subscriber" {
    string event_name "事件名"
    function callback_method "事件的方法"
  }
  
  "Event Channel" {
    string event_name "事件名"
    set subscribers "同名事件回调方法集合"
  }
  
  "Publisher" {
    string event_name "事件名"
    array data "传递参数数据"
  }
  
  "Subscriber" ||--o{ "Event Channel" : "subscribes"
  "Publisher" ||--o{ "Event Channel" : "publishes"
  "Event Channel" ||--o{ "Subscriber" : "triggers"

发布订阅模式实现

/**
 * EventCallback 事件回调函数。
 */
type EventCallback = (...args: any[]) => void;

/**
 * EventManager 管理事件的发布和订阅。
 */
class EventManager {
    /**
     * 私有静态属性,存储 EventManager 的单例实例。
     * @private
     */
    static #instance: EventManager;
    /**
     * 存储事件监听器的映射表,键为事件名称,值为该事件的监听器集合。
     * @private
     */
    #listeners: Record<string, Set<EventCallback>> = {};
    /**
     * 构造函数是私有的,以确保只能通过 getInstance 方法获取 EventManager 的实例。
     */
    private constructor() {}
    /**
     * 获取 EventManager 的单例实例。
     * @returns EventManager 的单例实例。
     */
    static getInstance() {
        return EventManager.#instance ??= new EventManager();
    }
    /**
     * 注册事件监听器。
     * @param event 事件名称。
     * @param listener 事件触发时调用的函数。
     */
    on(event: string, listener: EventCallback) {
        ;(this.#listeners[event]??= new Set()).add(listener);
    }
    /**
     * 触发指定事件,调用所有注册的监听器。
     * @param event 事件名称。
     * @param args 传递给监听器的参数。
     */
    emit(event: string, ...args: any[]) {
        this.#listeners[event]?.forEach(cb=>cb(...args))
    }
    /**
     * 移除事件监听器。
     * @param event 事件名称。
     * @param listener 要移除的监听器函数。
     */
    off(event: string, listener: EventCallback) {
        this.#listeners[event]?.delete(listener);
    }
    /**
     * 注册单次事件监听器,事件触发后自动移除。
     * @param event 事件名称。
     * @param listener 事件触发时调用的函数。
     */
    once(event: string, listener: EventCallback) {
        const wrapper = (...args: any[])=>{
            listener(...args);
            this.off(event, wrapper);
        }
        this.on(event, wrapper);
    }
}

FAQ🙋

Q: 如何实现先发布后订阅,也可以触发事件
A: 再维护一个事件参数对象,当发布事件(emit),如果还没有订阅者,可以将发布的事件名称和参数存在新维护的事件参数对象中,等订阅者订阅时,判断事件参数对象中的是否有对应事件,有即执行,执行结束删除该事件的事件参数对象,保证执行一次.

/**
 * 扩展需要根据自身业务需求来定义
 * 实现中使用 Set 存订阅事件及先发布后订阅的事件参数,想的是去重,
 * 重复订阅事件, 先发布后订阅多次相同参数的事件 都只存一次
 * 可以根据业务需求,来决定用Array存还是Set存
 */
type EventCallback = (...args: any[]) => void;

class EventManager {
    static #instance: EventManager;
    #listeners: Record<string, Set<EventCallback>> = {};
    #pending: Record<string, Set<any[]>> = {}
    private constructor() {}
    static getInstance() {
        return EventManager.#instance ??= new EventManager();
    }
    on(event: string, listener: EventCallback) {
        ;(this.#listeners[event]??= new Set()).add(listener);
        if(this.#pending[event]){
            this.#pending[event].forEach((args)=>listener(...args));
            delete this.#pending[event];
        }
    }
    emit(event: string, ...args: any[]) {
        if (this.#listeners[event]) {
            this.#listeners[event].forEach(cb=>cb(...args))
        } else {
            ;(this.#pending[event]??=new Set()).add(args)
        }
    }
    off(event: string, listener: EventCallback) {
        this.#listeners[event]?.delete(listener);
    }
    once(event: string, listener: EventCallback) {
        const wrapper = (...args: any[])=>{
            listener(...args);
            this.off(event, wrapper);
        }
        this.on(event, wrapper);
    }
}

Q: 如何实现先发布的事件设置有效期,在有效期内,有新订阅者就触发事件,超过有效期,再有新订阅者不触发事件
Q: 如何实现发布一条永久事件,不论什么时候新增订阅者都要处理这个事件


感谢阅读,敬请斧正!