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
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 in
emit()
Function.forEach
ormap
To make sure we can create new subscribers or unsubscribe. - When a
EventEmitter
Once the class is instantiated, it can pass a predefined event to the event interface. - You don’t need to use it
class
To achieve personal preference, but usingclass
It 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…