9
头图

Original: https://dushusir.com/js-event-bus/

introduce

Event Bus event bus is usually used as a communication mechanism between multiple modules, which is equivalent to an event management center. One module sends messages, and other modules receive messages, which achieves the function of communication.

For example, data passing between Vue components can use a Event Bus to communicate, or it can be used for plugin-to-core communication in the microkernel plugin system.

principle

Event Bus essentially adopts the publish-subscribe design pattern. For example, multiple modules A , B , C subscribe to an event EventX , and then a certain module X is responsible for notifying all subscribers of the event published on the event bus. A , B , C , they can all receive this notification message, and can also pass parameters.

// 关系图
                           模块X
                            ⬇发布EventX
╔════════════════════════════════════════════════════════════════════╗
║                         Event Bus                                  ║
║                                                                    ║
║         【EventX】       【EventY】       【EventZ】   ...           ║
╚════════════════════════════════════════════════════════════════════╝
  ⬆订阅EventX            ⬆订阅EventX           ⬆订阅EventX
 模块A                   模块B                  模块C

analyze

How to use JavaScript to implement a simple version of Event Bus

  • First construct a EventBus class, initialize an empty object to store all events
  • When accepting subscriptions, the event name is used as the key value, and the callback function that needs to be executed after accepting the published message is used as the value value. Since an event may have multiple subscribers, the callback function here should be stored as a list
  • When publishing an event message, get all the callback functions corresponding to the specified event name from the event list, and trigger and execute them in sequence

The following is the detailed implementation of the code, which can be copied to the Google Chrome console to run the detection effect directly.

code

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
  }
  // 发布事件
  publish(eventName) {
    // 取出当前事件所有的回调函数
    const callbackList = this.eventObject[eventName];

    if (!callbackList) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let callback of callbackList) {
      callback();
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // 存储订阅者的回调函数
    this.eventObject[eventName].push(callback);
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", () => {
  console.log("模块A");
});
eventBus.subscribe("eventX", () => {
  console.log("模块B");
});
eventBus.subscribe("eventX", () => {
  console.log("模块C");
});

// 发布事件eventX
eventBus.publish("eventX");

// 输出
> 模块A
> 模块B
> 模块C

We have implemented the most basic publish and subscribe functions above. In practical applications, there may be more advanced requirements.

Advanced

1. How to pass parameters when sending a message

The publisher passes in a parameter to EventBus , and then passes the parameter when the callback function of callback is executed, so that each subscriber can receive the parameter.

code

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackList = this.eventObject[eventName];

    if (!callbackList) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let callback of callbackList) {
      // 执行时传入参数
      callback(...args);
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // 存储订阅者的回调函数
    this.eventObject[eventName].push(callback);
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);


// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1

2. How to unsubscribe after subscribing

Sometimes subscribers only want to subscribe to messages in a certain period of time, which involves the ability to unsubscribe. We will revamp the code.

First of all, to achieve the specified subscriber unsubscribe, each time an event is subscribed, a unique unsubscribe function is generated. The user directly calls this function, and we delete the currently subscribed callback function.

// 每一次订阅事件,都生成唯一一个取消订阅的函数
const unSubscribe = () => {
  // 清除这个订阅者的回调函数
  delete this.eventObject[eventName][id];
};

Secondly, the subscribed callback function list is stored in an object structure, and a unique id is set for each callback function. When canceling the callback function, the efficiency of deletion can be improved. If you still use an array, you need to use split to delete it, which is not as efficient as the object's delete .

code

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
const subscriberC = eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 模块C取消订阅
subscriberC.unSubscribe();

// 再次发布事件eventX,模块C不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> 模块A {msg: 'EventX published again!'} 2
> 模块B {msg: 'EventX published again!'} 2

3. How to subscribe only once

If an event occurs only once, it usually only needs to be subscribed once, and there is no need to receive messages after receiving messages.

First, we provide an interface of subscribeOnce . The internal implementation is almost the same as that of subscribe . There is only one difference. Add a character callbackId in front of d to indicate that this is a subscription that needs to be deleted.

// 标示为只订阅一次的回调函数
const id = "d" + this.callbackId++;

Then, after executing the callback function, it is judged whether the current callback function's id is marked, and decides whether we need to delete the callback function.

// 只订阅一次的回调函数需要删除
if (id[0] === "d") {
  delete callbackObject[id];
}

code

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribeOnce("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 再次发布事件eventX,模块B只订阅了一次,不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块A {msg: 'EventX published again!'} 2
> 模块C {msg: 'EventX published again!'} 2

4. How to clear an event or all events

We also hope to clear all subscriptions of the specified event through an operation of clear , which is usually used when some components or modules are uninstalled.

  // 清除事件
  clear(eventName) {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this.eventObject = {};
      return;
    }

    // 清除指定事件
    delete this.eventObject[eventName];
  }

Similar to the logic of unsubscribing, except that it is handled uniformly here.

code

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 清除事件
  clear(eventName) {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this.eventObject = {};
      return;
    }

    // 清除指定事件
    delete this.eventObject[eventName];
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 清除
eventBus.clear("eventX");

// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> eventX not found!

5. TypeScript version

Given that TypeScript is now widely adopted, especially for large front-end projects, we briefly transform into a TypeScript version

You can copy the following code to TypeScript Playground to experience the running effect

code

interface ICallbackList {
  [id: string]: Function;
}

interface IEventObject {
  [eventName: string]: ICallbackList;
}

interface ISubscribe {
  unSubscribe: () => void;
}

interface IEventBus {
  publish<T extends any[]>(eventName: string, ...args: T): void;
  subscribe(eventName: string, callback: Function): ISubscribe;
  subscribeOnce(eventName: string, callback: Function): ISubscribe;
  clear(eventName: string): void;
}

class EventBus implements IEventBus {
  private _eventObject: IEventObject;
  private _callbackId: number;
  constructor() {
    // 初始化事件列表
    this._eventObject = {};
    // 回调函数列表的id
    this._callbackId = 0;
  }
  // 发布事件
  publish<T extends any[]>(eventName: string, ...args: T): void {
    // 取出当前事件所有的回调函数
    const callbackObject = this._eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName: string, callback: Function): ISubscribe {
    // 初始化这个事件
    if (!this._eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this._eventObject[eventName] = {};
    }

    const id = this._callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this._eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this._eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName: string, callback: Function): ISubscribe {
    // 初始化这个事件
    if (!this._eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this._eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this._callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this._eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this._eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 清除事件
  clear(eventName: string): void {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this._eventObject = {};
      return;
    }

    // 清除指定事件
    delete this._eventObject[eventName];
  }
}

// 测试
interface IObj {
  msg: string;
}

type PublishType = [IObj, number];

const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj: IObj, num: number, s: string) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 清除
eventBus.clear("eventX");

// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish<PublishType>("eventX", { msg: "EventX published again!" }, 2);

// 输出
[LOG]: "模块A",  {
  "msg": "EventX published!"
},  1
[LOG]: "模块B",  {
  "msg": "EventX published!"
},  1
[LOG]: "模块C",  {
  "msg": "EventX published!"
},  1
[WRN]: "eventX not found!"

6. Singleton Pattern

In the actual use process, only one event bus is often needed to meet the requirements. There are two cases here, the singleton and the global singleton are maintained in the upper-level instance.

  1. Keep the singleton in the upper instance

To introduce the event bus to the upper-level instance, it only needs to ensure that there is only one EventBus in an upper-level instance. If there are multiple upper-level instances, it means that there are multiple event buses, but each upper-level instance controls its own event bus.
First, a variable is established in the upper-level instance to store the event bus, which is only initialized when it is used for the first time, and the event bus instance is directly obtained when other modules use the event bus.

Code

// 上层实例
class LWebApp {
  private _eventBus?: EventBus;

  constructor() {}

  public getEventBus() {
    // 第一次初始化
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // 后续每次直接取唯一一个实例,保持在LWebApp实例中单例
    return this._eventBus;
  }
}

// 使用
const eventBus = new LWebApp().getEventBus();
  1. global singleton

Sometimes we hope that no matter which module wants to use our event bus, we all want these modules to use the same instance, which is a global singleton. This design makes it easier to manage events in a unified manner.

The writing method is similar to the above, the difference is to convert _eventBus and getEventBus to static properties. There is no need to instantiate the EventBusTool tool class when using it, just use the static method directly.

code

// 上层实例
class EventBusTool {
  private static _eventBus?: EventBus;

  constructor() {}

  public static getEventBus(): EventBus {
    // 第一次初始化
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // 后续每次直接取唯一一个实例,保持全局单例
    return this._eventBus;
  }
}

// 使用
const eventBus = EventBusTool.getEventBus();

Original: https://dushusir.com/js-event-bus/

Summarize

The above is the editor's understanding of Event Bus , which basically achieves the desired effect. By implementing the publish-subscribe model by yourself, it also deepens the understanding of the classic design pattern. There are still many shortcomings and areas that need to be optimized. You are welcome to share your experience.

refer to


Dushusir
125 声望4 粉丝

前端开发