技术规范

Runtime

  • NodeJs 14.16.0
  • npm 7.19.1
  • TypeScript 4.1.5

开发(设计)工具

  • Intellij IDEA

适用平台/语言

  • TypeScript
  • JavaScript
  • 可移植

术语

'''监听事件''' 由事件名称和监听器组成

'''监听器''' 与事件名称绑定的上下文逻辑处理函数

'''事件驱动实例''' 事件驱动器的实例化对象

'''异步''' 一种消息的通讯机制,当未接收到预期返回结果时,不会引发进程阻塞。当接收到预期返回结果,再执行与结果相关的处理函数。

基础设计

通常,只需要考虑如下方法实现即可:

'''on'''

用于注册一个监听事件。

'''once'''

用于注册一个一次性监听事件,事件被触发后,监听器执行一次即销毁。

'''off'''

通过事件名称注销该事件名称下的全部监听事件,或指定事件名称和监听器注销指定事件。

'''emit'''

通过事件名称触发监听事件,并传入监听器所需要的参数。

'''listener'''

用于指定监听名称下的监听事件,或获取整个事件驱动实例下的全部监听事件。

'''destory'''

用于销毁整个事件驱动实例。注销后,所有监听事件全部一并销毁。

进阶设计

返回值接收、匿名监听器注销的相关问题。

事件驱动的监听器,大都难以获取返回值,通过异步特性,可以在监听器注册处获取返回值以供上下文环境使用。

而对于需要注销的匿名函数,通常只有一种注销办法,就是将匿名函数赋值给一个常量或变量(let或const),使函数具名化或直接声明具名函数,然后在函数内部执行事件驱动实例的off方法将监听器本身注销。

'''emitAsPromise'''

通常情况下,通过emit触发监听事件以后,注册的监听器没有任何返回值。emitAsPromise将在用户注册操作的形式参数最后添加异步函数resolve、reject,以便于监听事件触发后,通过Promise返回监听器结果。

'''safeEmit'''

通过safeEmit触发的监听事件,将在监听器形式参数的最后添加一个finish方法,在监听器内执行该方法即可将该监听器注销。

'''safeEmitAsPromise'''

safeEmitAsPromise结合了emitAsPromise和safeEmit的特性,通过safeEmitAsPromise触发的监听事件,会在监听器形式参数的最后添加finish、resolve、reject方法。

高级设计(一)

监听事件归类注册、管理的相关问题。

一个典型例子,对于web socket的消息事件(消息事件遵循事件驱动原则),若想将不同的消息事件与其处理逻辑做隔离,按照单一事务原则做处理,就一定会将socket以组合的形式绑定到处理逻辑内部。假设存在一个点对点的socket消息处理,存在一个点对多的socket消息处理,对这两个处理进行拆分,按类(或函数)进行封装,那么,socket就要组合到两个类(或函数)中。此时,若其中任意一个类的实例(或函数)执行完毕,需要销毁,此时就存在了socket消息处理逻辑的销毁问题。

对于使用了事件驱动的诸如上述的情况或工厂模式等模型,往往都存在这样一个顶层的事件驱动实例,所有产品都向同一个事件驱动实例注册事件,但产品销毁的时候,注册在顶层事件驱动实例中的监听事件就变的难以销毁,若监听器内部的判断不当,反而会导致不应该注销的监听器被异常注销,已经注销的实例方法被错误的执行等等问题。

因此,实现下列方法以解决上述问题:

'''createChild'''

创建一个子事件驱动实例,并在事件驱动实例中添加该事件驱动实例,任何一个触发监听事件的方法被执行,都会使子事件驱动实例中相同事件名称的监听器被执行,如果子监听器有其自己的子监听器,则该触发会通过深度优先的方式向下传播,直到驱动器自身的监听事件被执行完毕。

'''removeChild'''

移除实例中指定的子事件驱动实例,移除后,子事件驱动实例(及其自身的子事件驱动实例)将全部被销毁。

'''clearChild'''

移除实例中全部的子事件驱动实例。

'''destory'''

用于销毁整个实例,如果该事件驱动实例存在父事件驱动实例,将在父事件驱动实例中注销该事件。事件驱动实例销毁后,事件驱动实例及其子事件实例(如果有)将一并销毁。

示例

以下为使用typescript语言设计的示例

type listener = (...args: any) => any

class SpreadEmitter {
  private readonly _parent: SpreadEmitter | null;

  private readonly _child: Set<SpreadEmitter>;

  private readonly _listeners: Map<string | symbol, Set<listener>>;

  constructor(parent?: SpreadEmitter) {
    if (parent) {
      this._parent = parent;
    } else {
      this._parent = null;
    }
    this._child = new Set<SpreadEmitter>();
    this._listeners = new Map<string | symbol, Set<listener>>();
  }

  public on(event: string | symbol, listener: listener): void {
    let listeners = this._listeners.get(event);
    if (!listeners) {
      listeners = new Set<listener>();
      this._listeners.set(event, listeners);
    }
    listeners.add(listener);
  }

  public once(event: string | symbol, listener: listener): void {
    let listeners = this._listeners.get(event);
    if (!listeners) {
      listeners = new Set<listener>();
      this._listeners.set(event, listeners);
    }
    const defer = (...args: any[]) => {
      listener.apply(this, args);
      this.off(event, defer);
    };
    listeners.add(defer);
  }

  public off(event: string | symbol, listener?: listener): void {
    if (!listener) {
      this._listeners.delete(event);
      return;
    }
    const listeners = this._listeners.get(event);
    if (listeners) {
      listeners.delete(listener);
    }
  }

  public emit(event: string | symbol, ...args: any[]): void {
    this._child.forEach((child) => {
      child.emit(event, ...args);
    });
    const listeners = this._listeners.get(event);
    if (!listeners) {
      return;
    }
    Object.assign([], Array.from(listeners)).forEach((listener: any) => {
      try {
        listener.apply(this, args);
      } catch (e) {
        this.off(event, listener);
        console.error("listener execute error, event: %s, listener: %o. error: %o", event, listener, e);
      }
    });
  }

  public emitAsPromise<T>(event: string | symbol, ...args: any[]): Promise<T | any> {
    return new Promise((resolve, reject) => {
      try {
        this.emit(event, ...args, resolve, reject);
      } catch (e) {
        console.error(e);
      }
    });
  }

  public safeEmit(event: string | symbol, ...args: any[]): void {
    this._child.forEach((child) => {
      child.safeEmit(event, ...args);
    });
    const listeners = this._listeners.get(event);
    if (!listeners) {
      return;
    }
    if (args === undefined) {
      args = [];
    }
    Object.assign([], Array.from(listeners)).forEach((listener: any) => {
      try {
        const callback = () => (this.off(event, listener));
        const [...params] = args;
        params.push(callback);
        listener.apply(this, params);
      } catch (e) {
        this.off(event, listener);
        console.error("listener execute error, event: %s, listener: %o. error: %o", event, listener, e);
      }
    });
  }

  public safeEmitAsPromise<T>(event: string | symbol, ...args: any[]): Promise<T | any> {
    return new Promise((resolve, reject) => {
      try {
        this.safeEmit(event, ...args, resolve, reject);
      } catch (e) {
        console.error(e);
      }
    });
  }

  public listeners(event?: string):
    Set<listener> | Map<string | symbol, Set<listener>> | undefined {
    if (event) {
      return this._listeners.get(event);
    }
    return this._listeners;
  }

  public createChild(): SpreadEmitter {
    const child = new SpreadEmitter(this);
    this._child.add(child);
    return child;
  }

  public removeChild(child: SpreadEmitter): boolean {
    return this._child.delete(child);
  }

  public clearChild(): void {
    return this._child.clear();
  }

  public destroy(): void {
    this._listeners.clear();
    this._parent?.removeChild(this);
    this._child.clear();
  }
}

export default SpreadEmitter;