An event emitter is an object that passes news from one part of a program to any other parts that want it. Code that wants to hear about something registers a function, called a listener, under an event name such as "message". Code that has news emits the event by that name, with any data, and the emitter calls every listener registered under it, passing the data along. Neither side needs to know about the other: the code that emits doesn't know who's listening, or whether anyone is, and listeners can be added and removed without touching it.
Think of a radio station. It broadcasts on its frequency whether anyone has tuned in or not, and every radio tuned to it hears the same thing. Tuning in is on, broadcasting is emit, and switching off is off.
Node has one built in, EventEmitter from the node:events module, and its four main methods are the ones most emitters share:
| Method | What it does |
|---|---|
on(name, listener) | Adds listener for the event name. An event can have any number of listeners. |
once(name, listener) | Adds a listener that runs the next time name is emitted, and is then removed. |
off(name, listener) | Removes a listener. You pass the same function you added, so keep a reference to it: an arrow function written inline can't be removed later. Some emitters also have on return a function that removes the listener, so you don't need to keep it. |
emit(name, ...args) | Calls each of the event's listeners with args, one after another in the order they were added, before emit returns. In Node, a listener written as a regular function gets the emitter as this, and emit returns true if the event had listeners and false if it had none. |
const { EventEmitter } = require("node:events");
const chat = new EventEmitter();
const greet = (name) => console.log("Hello, " + name);
chat.on("join", greet);
chat.once("join", (name) => console.log(name + " was first"));
chat.emit("join", "Ada"); // logs "Hello, Ada", then "Ada was first"; returns true
chat.emit("join", "Alan"); // logs "Hello, Alan" only: the once listener is gone
chat.off("join", greet);
chat.emit("join", "Grace"); // logs nothing and returns false: no listeners are left
In the browser, the same idea is EventTarget, which every element, document, and window inherit from. addEventListener(type, listener) is its on, removeEventListener(type, listener) its off, and addEventListener(type, listener, { once: true }) its once. The browser emits events such as click and input itself, and your code can emit its own with dispatchEvent. Node's streams and servers are EventEmitters too, which is why you write stream.on("data", ...).
A publish/subscribe hub, pub/sub for short, is the same idea under other names. Event names are called topics, often strings such as "user.created"; adding a listener is subscribing, and emitting is publishing. Some hubs also let one subscription match a whole group of topics with a wildcard pattern. An event bus is one emitter or hub that many parts of an app share, so they can talk to each other without importing each other.
Write a class EventEmitter with four methods.
on(event, listener) adds a listener for event and returns a function that removes it. off(event, listener) removes a listener, including one added with once. once(event, listener) adds a listener that runs on the next emit of that event only.
emit(event, ...args) calls the event's listeners with args, in the order they were added, with this set to the emitter. It returns true if the event had listeners and false if it had none. Listeners added or removed while an emit is running don't change who runs in that emit: the list is fixed when it starts.
const emitter = new EventEmitter();
const stop = emitter.on("message", (text) => console.log("got", text));
emitter.once("message", () => console.log("first message only"));
emitter.emit("message", "hi"); // logs "got hi", then "first message only"
emitter.emit("message", "yo"); // logs "got yo"
stop();
emitter.emit("message", "?"); // false: no listeners left
const emitter = new EventEmitter();
const log = [];
emitter.on("x", (a, b) => log.push(["first", a, b]));
emitter.on("x", (a) => log.push(["second", a]));
emitter.emit("x", 1, 2);
expect(log).toEqual([["first", 1, 2], ["second", 1]]);
const emitter = new EventEmitter();
emitter.on("a", () => {});
expect(emitter.emit("a")).toBe(true);
expect(emitter.emit("b")).toBe(false);
const stop = emitter.on("c", () => {});
stop();
expect(emitter.emit("c")).toBe(false);
const emitter = new EventEmitter();
const listener = spy();
const onceListener = spy();
emitter.on("x", listener);
emitter.once("y", onceListener);
emitter.emit("x");
emitter.emit("y");
expect(listener).toHaveBeenCalledTimes(1);
expect(listener.calls[0].this).toBe(emitter);
expect(onceListener).toHaveBeenCalledTimes(1);
expect(onceListener.calls[0].this).toBe(emitter);
const emitter = new EventEmitter();
const removed = spy();
const kept = spy();
emitter.on("x", removed);
emitter.on("x", kept);
emitter.off("x", removed);
emitter.emit("x");
expect(removed).not.toHaveBeenCalled();
expect(kept).toHaveBeenCalledTimes(1);
const emitter = new EventEmitter();
const listener = spy();
const stop = emitter.on("x", listener);
emitter.emit("x");
stop();
emitter.emit("x");
expect(listener).toHaveBeenCalledTimes(1);
const emitter = new EventEmitter();
const listener = spy();
emitter.once("x", listener);
emitter.emit("x", 1);
emitter.emit("x", 2);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(1);
expect(emitter.emit("x")).toBe(false);
let runs = 0;
emitter.once("y", () => {
runs++;
emitter.emit("y");
});
emitter.emit("y");
expect(runs).toBe(1);
const emitter = new EventEmitter();
const listener = spy();
emitter.once("x", listener);
emitter.off("x", listener);
expect(emitter.emit("x")).toBe(false);
expect(listener).not.toHaveBeenCalled();
const emitter = new EventEmitter();
const log = [];
const b = () => log.push("b");
emitter.on("x", () => {
log.push("a");
emitter.off("x", b);
});
emitter.on("x", b);
emitter.on("x", () => log.push("c"));
emitter.emit("x");
emitter.emit("x");
expect(log).toEqual(["a", "b", "c", "a", "c"]);
const emitter = new EventEmitter();
const late = spy();
emitter.on("x", () => emitter.on("x", late));
emitter.emit("x");
expect(late).not.toHaveBeenCalled();
emitter.emit("x");
expect(late).toHaveBeenCalledTimes(1);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Event emitter. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.