The background,

Let’s start with a DOM event:

const button = document.querySelector("button");

button.addEventListener("click", (event) => /* do something with the event */)
Copy the code

This code adds an event listener to the button. Each time the button is clicked, the click event is triggered and the callback function is called.

There are many times when you need to fire custom events, not just a click event, but an event emitter that needs to fire an event based on another trigger, and that needs to respond to an event.

An Event Emitter is a pattern in which an event emitter listens for an event, fires a callback, and emits an event with a value. It is sometimes called the PUB/SUB model or listener.

One implementation in JavaScript is as follows:

let n = 0;
const event = new EventEmitter();

event.subscribe("THUNDER_ON_THE_MOUNTAIN", value => (n = value));

event.emit("THUNDER_ON_THE_MOUNTAIN", 18);

// n: 18

event.emit("THUNDER_ON_THE_MOUNTAIN", 5);

// n: 5
Copy the code

In the above code, we subscribe to an event called THUNDER_ON_THE_MOUNTAIN, and even the emitted emitted event must be emitted by a callback called value => (n = value) when an event is emitted.

This is useful when interacting with asynchronous code if there are values that need to be updated that are not in the current module.

A real example is React Redux. Redux requires a mechanism for notifying the outside world that its internal values have been updated. It allows React to call setState() and rerender the UI to see which values have changed. Redux Store has a subscription function that takes as argument a callback function that provides the new store. In this subscription function, the React Redux component that calls the setState() method with the value of the new store can be viewed here.

Now we have two different parts of the app, the React UI and the Redux Store, and it’s not clear which part triggers events.

Second, the implementation

Let’s start with a simple Event Emitter that uses a class to track events.

class EventEmitter {
  public events: Events;
  constructor(events?: Events) {
    this.events = events || {};
  }
}
Copy the code
  • The event

Define an event interface to store a blank object in which each key is an event name and the respective value is an array of callback functions.

interface Events { [key: string]: Function[]; {} / * *"event": [fn],
  "event_two": [fn]
}
*/
Copy the code

Arrays are used because there can be multiple subscriber per event, because element.addeventlister (“click”) can be called multiple times.

  • To subscribe to

Now you need to handle the subscribed event. In the above example, the subscribe() function takes two arguments: a name and a callback function.

event.subscribe("named event", value => value);
Copy the code

Define a SUBSCRIBE method to accept these two arguments, just add them to this.events inside the class.

class EventEmitter {
  public events: Events;
  constructor(events?: Events) {
    this.events = events || {};
  }

  public subscribe(name: string, cb: Function) {
    (this.events[name] || (this.events[name] = [])).push(cb);
  }
}
Copy the code
  • launch

When a new event is emitted, the callback function is fired. When emitted, the name of the event stored in (emit(“event”)) and any value that needs to be passed to the callback function (emit(“event”, value)) are used. We can simply pass any argument to the callback function after the first argument.

class EventEmitter { public events: Events; constructor(events? : Events) { this.events = events || {}; } public subscribe(name: string, cb: Function) { (this.events[name] || (this.events[name] = [])).push(cb); } public emit(name: string, ... args: any[]): void { (this.events[name] || []).forEach(fn => fn(... args)); }}Copy the code

Now that we know the events we want to emit, we can view them using this.events[name], which returns an array of callback functions.

  • unsubscribe
subscribe(name: string, cb: Function) {
  (this.events[name] || (this.events[name] = [])).push(cb);

  return {
    unsubscribe: () =>
      this.events[name] && this.events[name].splice(this.events[name].indexOf(cb) >>> 0, 1)
  };
}
Copy the code

The above code returns an object with an unsubscribe method. We can use the arrow function () => to get the scope of the argument passed to the parent. In this function, we can use the >>> operator to find the index passed to the parent callback. This ensures that splice() will always return a real number every time we call it on the callback array, even if indexOf() does not return a number. It can be used like this:

const subscription = event.subscribe("event", value => value);

subscription.unsubscribe();
Copy the code

At this point, we can cancel this particular subscription without affecting the others.

  • Complete implementation
interface Events {
  [key: string]: Function[];
}

exportclass EventEmitter { public events: Events; constructor(events? : Events) { this.events = events || {}; } public subscribe(name: string, cb: Function) { (this.events[name] || (this.events[name] = [])).push(cb);return{ unsubscribe: () => this.events[name] && this.events[name].splice(this.events[name].indexOf(cb) >>> 0, 1) }; } public emit(name: string, ... args: any[]): void { (this.events[name] || []).forEach(fn => fn(... args)); }}Copy the code
  • The instance

Codepen. IO/charliewilc…

In the above code, Event Emitter is first used in another event callback. In this case, an Event Emitter is used to clean up some logic, select a repository on GitHub, fetch the details, cache the details, and update the DOM to display them. The subscription callback gets the results from the network or cache and updates them. The reason for this is that we give the callback a random repository from the list when we emit the time.

Now consider something a little different. In an application, there may be many states that need to be triggered after logging in, and there may be multiple subscribers to handle user attempts to log out. Because an event with a false value has been emitted, each subscriber can use this value and needs to determine whether it needs to redirect the page, remove the cookie, or disable the form.

const events = new EventEmitter();

events.emit("authentication".false);

events.subscribe("authentication", isLoggedIn => {
  buttonEl.setAttribute("disabled", !isLogged);
});

events.subscribe("authentication", isLoggedIn => { window.location.replace(! isLoggedIn ?"/login" : "");
});

events.subscribe("authentication", isLoggedIn => { ! isLoggedIn && cookies.remove("auth_token");
});
Copy the code
  • The last

To get emitters to work, there are a few things to consider:

  • Need to be inemit()Function.forEachormapTo make sure we can create new subscribers or unsubscribe.
  • When aEventEmitterOnce the class is instantiated, it can pass a predefined event to the event interface.
  • You don’t need to use itclassTo achieve personal preference, but usingclassIt makes it clearer where events are stored. It can be implemented in a function like this:
functionemitter(e? : Events) {let events: Events = e || {};

  return {
    events,
    subscribe: (name: string, cb: Function) => {
      (events[name] || (events[name] = [])).push(cb);

      return {
        unsubscribe: () => {
          events[name] && events[name].splice(events[name].indexOf(cb) >>> 0, 1);
        }
      };
    },
    emit: (name: string, ...args: any[]) => {
      (events[name] || []).forEach(fn => fn(...args));
    }
  };
}
Copy the code

reference

Css-tricks.com/understandi…