preface

EventEmitter, widely used in javascript, and browser events such as mouse click and keyboard keyDown are examples of this pattern. We often use it for decoupling when we write code, for example, when we don’t want to communicate between components through a lot of states. Not only that, the interviewer may ask us to implement an EventEmitter during the interview, which many people don’t.

There are already many implementations of EventEmitter on the web, so why write one; First of all, it is better to write once than to read it a hundred times. Secondly, this module is very common, and it is easy to modify according to the actual business situation if you customize it.

Code implementation

Using Nodejs’s EventEmitter API as a reference, we’ll probably implement the following apis:

  • On () // Listen on events
  • Once () // Listen for the event once
  • Emit () // Fires the event
  • Off () // Cancel listening
  • GetEvents () // gets all listening events

First we need a place to store all listening events, so how do we store them? A common approach is to use hash tables, because the time complexity is O(1) and the space complexity is generally not very large. So our storage method is:

interface EventType {
  readonly callback: Function;
  readonly once: boolean;
}

interface EventMap {
  [propName: string]: EventType[]
}

export default class EventEmitter {
  private eventMap: EventMap = {};
}
Copy the code

The next step is to implement the concrete method

Listen for an event

// Listen for events on(event: string, callback: Function, once? : boolean) { if (! this.eventMap[event]) { this.eventMap[event] = []; } this.eventMap[event].push({ callback, once: !! once, }); return this; Return this. On (event, callback, true); }Copy the code

There are two things to note here. First of all, the initial value of each event is an array, because we can do multiple things with one event; Second, listen for the event once, but call the listener event method with an extra parameter. For why, we will see the trigger event method later.

Triggering event

// Emit events emit(event: string,... args: any[]) { const events = this.eventMap[event] || []; let length = events.length; for (let i = 0; i < length; i++) { if (! events[i]) { continue; } const { callback, once } = events[i]; if (once) { events.splice(i, 1); if (events.length === 0) { delete this.eventMap[event]; } length--; i--; } callback.apply(this, args); }}Copy the code

Here we treat event listening differently in two different cases.

Cancel event listening

// Cancel event listener off(event? : string, callback? : Function) { if(! EventMap = {}} else {if(! Delete this.eventMap[event]} else {// Event exists, callback exists, To get rid of the matching method is const events = this. EventMap [event] | | []; let length = events.length; for (let i = 0; i < length; i++) { if (events[i].callback === callback) { events.splice(i, 1); length--; i--; } } if (events.length === 0) { delete this.eventMap[event]; } } } return this; }Copy the code

The cancellation event listening here is divided into several different cases, according to the parameters to determine.

The complete code

interface EventType { readonly callback: Function; readonly once: boolean; } interface EventMap { [propName: string]: EventType[] } export default class EventEmitter { private eventMap: EventMap = {}; // Listen for events on(event: string, callback: Function, once? : boolean) { if (! this.eventMap[event]) { this.eventMap[event] = []; } this.eventMap[event].push({ callback, once: !! once, }); return this; Return this. On (event, callback, true); } // Emit the event emit(event: string,... args: any[]) { const events = this.eventMap[event] || []; let length = events.length; for (let i = 0; i < length; i++) { if (! events[i]) { continue; } const { callback, once } = events[i]; if (once) { events.splice(i, 1); if (events.length === 0) { delete this.eventMap[event]; } length--; i--; } callback.apply(this, args); }} // Cancel event listener off(event? : string, callback? : Function) { if(! EventMap = {}} else {if(! Delete this.eventMap[event]} else {// Event exists, callback exists, To get rid of the matching method is const events = this. EventMap [event] | | []; let length = events.length; for (let i = 0; i < length; i++) { if (events[i].callback === callback) { events.splice(i, 1); length--; i--; } } if (events.length === 0) { delete this.eventMap[event]; } } } return this; } // Get all current events getEvents() {return this.eventmap; }}Copy the code

After reading mine, I hope you can also start to realize one, their own writing is not easy to forget.