Observer & pub/sub
Learn observer, pub/sub, event emitters, signals, and observables to let JavaScript app parts react without tight coupling.
- 01Let parts of an app react to a changeReplace one function that calls every job by name with listeners that sign up to be told.
- 02Pick the right toolTell apart observer, pub/sub, browser events, Node events, signals, and observables.
- 03Avoid common problemsKeep listeners in order, stop one error from breaking the rest, remove listeners when done, and keep the flow easy to follow.
Let many parts of an app react to one change
Apps often need several things to happen after one change. Say a shopper adds a book to the cart. Then:
- the header shows the new number of items,
- analytics, the code that records what shoppers do, notes the book,
- storage saves the cart, so it is still there after a page reload.
The easy way is to make the cart code do all three jobs itself. That works. But then the cart code has to know about every job. This lesson shows better ways.
All of them use the same idea. One piece of code says "something changed." Other pieces of code ask to be told. Those are called listeners. A listener is just a function that waits to be called. Code also calls listeners observers, subscribers, or handlers. In this lesson, these four words mean the same thing.
- Observer pattern: one object, called the subject, keeps a list of listeners. When it changes, it calls each one. Calling them all is called notifying.
- Publish/subscribe, or pub/sub: a middle object, called a bus, keeps the lists. Each list has a name. The code that sends a message only gives the bus a name and some data.
- Event emitters, signals, and observables are other forms of the same idea. Each one gets its own section below.
The table below puts the five tools side by side. Read each row as: "this tool works like this, and here is a tiny example." Some words are new; each one is explained in its own section.
| Tool | How it works | A tiny example |
|---|---|---|
| Observer | One object, the subject, keeps a list of listeners. It calls them when it changes. | A cart calls its header, analytics, and storage listeners, in the order they joined. |
| Pub/sub | A bus keeps a list of listeners for each topic name. The publisher only knows the topic name. | bus.publish("cart:changed", payload) reaches every listener on that topic. |
| EventTarget | The browser's built-in event system. Listeners get an Event or CustomEvent object. | target.dispatchEvent(new CustomEvent(...)) works with once and AbortController. |
| Signals | A value that remembers who read it. When it changes, only that code runs again. | total = computed(() => price() * quantity()) updates when either signal changes. |
| Observables | A stream that sends values one after another, over time. | RxJS streams, or Chrome's when() method on an EventTarget. |
This lesson uses ideas from earlier lessons: callbacks, closures, events, and custom events. The functors & monads lesson built a tiny observable. The memory management lesson explains why forgotten listeners waste memory.
The problem: a cart that knows too much
PROBLEM FIRSTTwo pieces of code are tightly coupled when one has to know the other by name and call it directly. Here is a cart update that is tightly coupled to three other jobs.
const cart = []; function updateCart(item) { cart.push(item); renderHeader(cart.length); trackAnalytics(item); saveCart(cart);} function renderHeader(count) { console.log(`header ${count}`);}function trackAnalytics(item) { console.log(`analytics ${item}`);}function saveCart(items) { console.log(`saved ${items.join(",")}`);} updateCart("book");Line 4 adds the item to the cart. Line 5 updates the header. Line 6 tells analytics. Line 7 saves the cart. Line 20 calls updateCart("book"). The console shows header 1, analytics book, and saved book.
updateCart() calls all three jobs by name. To add a fourth job, you must change updateCart() again.So updateCart has four jobs, not one. Each new job means changing it again. And to test the cart, you also need the header, analytics, and storage code.
We want the cart code to say only "the cart changed." Each job should sign up for that news from its own file. Then the cart doesn't need to know which jobs exist, today or next year.
Observer: the subject keeps a list of listeners
STEP THROUGHIn the observer pattern, the subject has a subscribe function. To subscribe means to join the list. You pass in a listener function, and the subject adds it to its list.
subscribe gives back another function. Call that one later to leave the list. Leaving the list is called unsubscribing.
The code below keeps the list in a Set. A Set is a collection that holds each value only once. It also remembers the order you added them in.
const listeners = new Set();function subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener);}const stopHeader = subscribe((item) => console.log(`header ${item}`));subscribe((item) => console.log(`storage ${item}`));for (const listener of [...listeners]) listener("tea");stopHeader();console.log(listeners.size);- Lines 2 to 5 make
subscribe. Line 3 adds the listener to the set. Line 4 returns a small function that deletes that same listener. - Line 6 subscribes the header listener. It keeps the leave function in
stopHeader. Line 7 subscribes the storage listener. - Line 8 calls every listener with
"tea". The header joined first, so it printsheader teafirst. Then storage printsstorage tea. - Line 9 calls
stopHeader(), which removes the header. Line 10 prints1, because only storage is left.
listeners set holds two functions. Line 8 gives "tea" to each one, in the order they joined: header first, then storage.On line 8, [...listeners] copies the set into a new array before the loop starts. You'll see why that copy matters in a moment.
A newsletter keeps a list of readers. Readers don't change the newsletter. They only sign up or cancel. When a new issue is ready, the newsletter sends it to everyone on the list that day.
- In real life: A reader signs up
- In JavaScript:
subscribe(listener)adds the function to the list - In real life: The newsletter sends a new issue
- In JavaScript: The subject calls each listener with the new value
- In real life: A reader cancels
- In JavaScript: Calling the function that
subscribereturned removes the listener
A subject in a real app needs two safety steps:
- Copy the list before calling anyone, with
[...observers]. Then a listener that leaves halfway through doesn't change the loop that is running. - Catch an error from one listener, so the next listeners still run.
try/catchdoes this. If code insidetrythrows an error, JavaScript jumps tocatchinstead of stopping.
Step through the example below. Watch the storage listener remove itself during the first call to notify.
Step through the order in which listeners run. Watch the copied list, the caught error, and how storage leaving changes only the next notify.
script
function createSubject(name) { const observers = new Set(); return { subscribe(observer) { observers.add(observer); return () => observers.delete(observer); }, notify(value) { for (const observer of [...observers]) { try { observer(value); } catch (error) { console.log(`${name} isolated ${error.message}`); } } }, count() { return observers.size; }, };} let stopStorage;cart.subscribe((item) => console.log(`header ${item}`));cart.subscribe(() => { throw new Error("analytics offline"); });stopStorage = cart.subscribe((item) => { console.log(`storage ${item}`); stopStorage();});cart.notify("book");cart.notify("pen");console.log(`subscribers ${cart.count()}`);Here is the order. The header runs. Analytics throws an error, and the catch prints it. Storage still runs, and then removes itself. The second notify reaches only the header and analytics. This works because of the copied list and the try/catch around each listener.
Next, one listener removes a different listener while the subject is calling everyone.
const listeners = new Set();function subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener);}let stopStorage = () => {};subscribe((item) => { console.log(`header ${item}`); if (item === "first") stopStorage();});stopStorage = subscribe((item) => console.log(`storage ${item}`));function notify(value) { for (const listener of [...listeners]) listener(value);}notify("first");notify("second");console.log(listeners.size);Line 13 copies the set before it calls anyone. On the first call, line 15, the header runs first. Line 9 then removes storage from the set. But the copy already has storage in it, so storage still prints storage first.
On the second call, line 16, a new copy is made without storage. So only header second prints. Line 17 prints 1.
notify() copies the set first. The header removes storage during the first call, but copy 1 already has storage in it. Copy 2 does not.const observers = [ () => console.log("header updated"), () => { throw new Error("offline"); }, () => console.log("storage saved"),];for (const observer of [...observers]) { try { observer(); } catch (error) { console.log(`reported ${error.message}`); }}Line 3 is a listener that throws an error with the message offline. The try/catch on lines 7 to 11 sits inside the loop, around each single call. So line 10 prints reported offline, and the loop moves on. The last listener still prints storage saved.
- A simple loop like this calls listeners right away, one after another, in the order they joined.
- If a listener is removed while the loop runs over a copy, it still runs this time, but not next time.
try/catchcan't catch an error from anasynclistener. Anasyncfunction returns a promise, and its error arrives later, after the loop has moved on.- A
Setholds the same function only once. But two arrow functions that look alike are two different functions, so the set keeps both.
Publish/subscribe: a bus in the middle
TOPICSPub/sub goes one step further than observer. The code that announces a change is called the publisher. In pub/sub, the publisher doesn't keep a list at all. A third object, the bus, keeps the lists. (Some code calls the bus a broker.)
- Each list in the bus has a name, called a topic, such as
cart:changed. - Listeners subscribe to a topic.
- The publisher gives the bus a topic and some data. That data is called the payload.
A radio station plays music on one channel. People tune their radios to the channel they want. The station has no list of who is listening. It just plays.
- In real life: The station plays on one channel
- In JavaScript: The publisher calls
publish(topic, payload) - In real life: People tune in to the channel they want
- In JavaScript: Listeners subscribe to a topic
- In real life: The station doesn't know who is listening
- In JavaScript: The publisher doesn't know who subscribed
const cart = { observers: new Set() };cart.observers.add((item) => console.log(`observer ${item}`));for (const observer of cart.observers) observer("tea"); const broker = new Map();broker.set("cart:changed", [(item) => console.log(`pub/sub ${item}`)]);for (const handler of broker.get("cart:changed")) handler("tea");Lines 1 to 3 are the observer way. The list lives inside cart, so the cart knows its listeners.
Lines 5 to 7 are the pub/sub way. broker is a Map, a collection of named entries. The list lives in it under the name cart:changed. Code only needs that name to reach the listeners. Both ways print their label with tea.
The bus has a cost. After a publish, it is harder to find which code runs, because the listeners live somewhere else.
function createBus() { const topics = new Map(); return { subscribe(topic, handler) { const handlers = topics.get(topic) ?? new Set(); handlers.add(handler); topics.set(topic, handlers); return () => handlers.delete(handler); }, publish(topic, payload) { for (const handler of topics.get(topic) ?? []) { handler(payload); } }, };} const bus = createBus();bus.subscribe("cart:changed", ({ count }) => console.log(`header ${count}`));bus.subscribe("analytics:cart", ({ sku }) => console.log(`analytics ${sku}`));bus.publish("cart:changed", { count: 2, sku: "book" });bus.publish("analytics:cart", { count: 2, sku: "book" });- Line 4 is
subscribe. It adds a listener to the list for one topic. - Line 10 is
publish. It calls only the listeners for that exact topic. - Line 19 subscribes the header to
cart:changed. Line 20 subscribes analytics toanalytics:cart. - Line 21 publishes on
cart:changed. Only the header runs, and it printsheader 2. - Line 22 publishes on
analytics:cart. Only analytics runs, and it printsanalytics book.
cart:changed. The bus looks up that one topic, so only the header function runs and prints header 2.Give topics clear names that start with the area they belong to, like cart:changed, analytics:cart, or cart:item:removed. Then you can search your code for them. Some buses let one listener subscribe to many topics with a pattern such as cart:*. Use that with care: it makes it harder to see which code runs.
Pub/sub works well when parts of an app should not import each other. Examples are plugins, analytics, and messages between separate parts of a page. It is also easy to overuse. A long chain of hidden events is hard to debug. And a listener you forget to remove can cause a memory leak: memory that is never freed.
Event emitters in the browser and in Node
PLATFORM APIsAn event emitter is an object that sends named events to its listeners. You don't have to build one yourself. The browser and Node each come with one.
In the browser, it is EventTarget. Every element on a page is an EventTarget. That is how clicks reach your listeners. You can also make a plain one with new EventTarget().
To send your own event, make a CustomEvent and put the data in its detail option. The custom events lesson shows more.
To remove many listeners at once, use an AbortController. Pass its signal when you add each listener. Later, one call to abort() removes all of them.
const target = new EventTarget();const group = new AbortController(); target.addEventListener("cart:changed", (event) => { console.log(`header ${event.detail.count}`);}, { signal: group.signal }); target.addEventListener("cart:changed", (event) => { console.log(`analytics ${event.detail.sku}`);}, { once: true }); target.dispatchEvent(new CustomEvent("cart:changed", { detail: { count: 1, sku: "book" },}));group.abort();target.dispatchEvent(new CustomEvent("cart:changed", { detail: { count: 2, sku: "pen" },}));- Lines 4 to 6 add a header listener tied to
group.signal. - Lines 8 to 10 add an analytics listener with
{ once: true }. That means "remove me after I run once." - Line 12 sends the first event. Both listeners run, so the console shows
header 1andanalytics book. The analytics listener then removes itself. - Line 15 calls
group.abort(), which removes the header listener. - So the second event, on line 16, prints nothing. No listeners are left.
dispatchEvent() calls every listener right away, before it returns. It doesn't wait.
Node's version is called EventEmitter. Its method names are different:
onadds a listener.onceadds a listener that runs only once.offremoves a listener.emitsends an event.
Two more things to know. First, an event named error is special. If you emit it and no listener handles it, Node throws an error. Second, Node prints a warning when you add too many listeners to one event. That warning often means some code forgot to remove its listeners.
import { EventEmitter } from "node:events"; const bus = new EventEmitter();function header({ count }) { console.log(`header ${count}`);}function storage({ count }) { console.log(`storage ${count}`);} bus.on("cart:changed", header);bus.once("cart:changed", ({ count }) => console.log(`analytics first ${count}`));bus.on("cart:changed", storage);bus.emit("cart:changed", { count: 1 });bus.off("cart:changed", storage);bus.emit("cart:changed", { count: 2 }); const errors = new EventEmitter();errors.on("error", (error) => console.log(`handled ${error.message}`));errors.emit("error", new Error("offline")); const noisy = new EventEmitter();noisy.setMaxListeners(1);noisy.on("warn", () => {});noisy.on("warn", () => {});console.log(`listeners ${noisy.listenerCount("warn")}`);- Line 14 emits the first event. The header, the once-only analytics listener, and storage all run.
- Line 15 removes storage, so line 16 reaches only the header.
- Line 20 emits
error. The listener on line 19 handles it, so nothing throws. - Line 23 sets the limit to one listener, and lines 24 and 25 add two. So Node prints a warning, and line 26 prints
listeners 2.
node:events is part of Node, not the browser, so this page can't run it. In Node, it prints header 1, analytics first 1, storage 1, header 2, handled offline, and listeners 2. Node also prints a MaxListenersExceededWarning.
Signals: a value that knows who uses it
REAL RUNTIMEA signal holds one current value. You call it to read the value, like price(). You change it with .set(). The clever part: a signal remembers which code read it. When the value changes, only that code runs again.
Two more words come with signals:
- A computed value is worked out from other signals. For example,
totalisprice()timesquantity(). - An effect is code that runs again whenever a signal it read changes. For example, code that prints the total.
So when quantity changes, only total and the code that prints it run again. Nothing else in the app does.
The tiny version below is real, working JavaScript. Here is how it knows who read a signal:
- Before an effect runs, it puts itself on top of a list called
effectStack. - Any signal that is read while the effect runs looks at the top of that list. It adds that effect to its own subscribers.
- When the effect finishes, it takes itself off the list.
The code also does two tidy-up jobs. It clears an effect's old subscriptions before the effect runs again. And it copies a signal's subscriber list before calling it. Real signal libraries add more, such as grouping many updates into one, but the main idea is all here.
This tiny signals code uses an effect stack to see who reads each signal. Step through the first calculation, then the change that makes total and the printing effect run again.
script
const effectStack = []; function cleanup(runner) { for (const dependency of runner.dependencies) { dependency.delete(runner); } runner.dependencies.clear();} function effect(work) { const runner = () => { cleanup(runner); effectStack.push(runner); try { work(); } finally { effectStack.pop(); } }; runner.dependencies = new Set(); runner(); return () => cleanup(runner);} function signal(value) { const subscribers = new Set(); const read = () => { const runner = effectStack[effectStack.length - 1]; if (runner) { subscribers.add(runner); runner.dependencies.add(subscribers); } return value; }; read.set = (next) => { if (Object.is(next, value)) return; value = next; for (const runner of [...subscribers]) runner(); }; return read;} function computed(work) { const output = signal(); effect(() => output.set(work())); return output;} const quantity = signal(2);const total = computed(() => price() * quantity());effect(() => console.log(`total ${total()}`));quantity.set(3);You'll find signals in Solid, Preact Signals, and Angular. Vue's ref works in a similar way. There is a proposal to add signals to JavaScript itself, but it is only at Stage 1, an early step. So today, each framework gives you its own signals.
Observables: values that arrive over time
STREAMSAn observable is a stream: a source that sends values one after another, over time. Clicks, messages from a server, timer ticks, and a file arriving in pieces are all streams.
A listener gets each value through a function called next. It keeps getting values until it unsubscribes or the stream ends. RxJS is the most common library for observables.
The functors & monads lesson built a tiny observable. This lesson won't build one again. A later project lesson builds a full event emitter from scratch. Here, just learn the difference:
- A signal answers "what is the value now?"
- An observable answers "what values will arrive over time?"
console.log(typeof EventTarget.prototype.when);checking
The check runs as soon as the page has loaded.
when is. In Chrome it prints function. If you need observables in every browser, use the RxJS library.Browsers are adding observables too, through a proposal called the Observable API. In Chrome, every EventTarget already has a when() method that gives you an observable of its events. Other browsers may not have it yet. So check first, as the code above does. Checking whether a browser has a feature before you use it is called feature detection.
If you need observables in every browser today, use RxJS or your framework's tools.
Choose the right tool
PLAYGROUNDPick the smallest tool that fits. Start by asking who should own the list of listeners:
- If one object owns the data, use observer.
- If separate parts of the app should talk without importing each other, use pub/sub. The cost is that the flow is harder to follow.
- If you are in the browser or in Node, use the event emitter that comes with it.
- If you need values worked out from other values, use signals.
Read each row of the table as: "in this situation, this tool fits, for this reason."
| Situation | Tool that fits | Why |
|---|---|---|
| One object owns the data, and listeners can know about it | Observer | The subject can offer subscribe, call each listener, and give back an unsubscribe function. |
| Parts of the app should talk without importing each other | Pub/sub | Topics keep the publisher and the listeners apart. The cost: the flow is harder to follow. |
| Browser pages and page elements | EventTarget | It is built in. Use CustomEvent, once, and AbortController. |
| A value worked out from other values on the page | Signals | Only the code that read the value runs again. |
| Values that keep arriving, and that you may want to stop | Observable | A stream sends values over time. You can change them with steps like map, filter, and take. |
A listener can also keep other things in memory. Say a listener uses a profile name or a page element. While the listener is in the list, that name or element stays in memory too, even after the profile is closed. So remove listeners when you are done with them.
The small example below counts listeners. It doesn't measure memory.
const listeners = new Set();function mountProfile(name) { const listener = () => console.log(name); listeners.add(listener); return () => listeners.delete(listener);}const closeProfile = mountProfile("Asha");console.log(listeners.size);closeProfile();console.log(listeners.size);- Line 3 makes a listener that uses
name. - Line 4 adds it to the set, so line 8 prints
1. - Line 9 calls
closeProfile(). That is the functionmountProfilereturned, and it removes the listener. So line 10 prints0.
In a real page, call this cleanup function when the profile is taken off the page. JavaScript then frees the memory later, at a time it chooses. You can't make it free memory right now.
const subscribers = new Set();const store = { count: 0, subscribe(widget) { subscribers.add(widget); return () => subscribers.delete(widget); }, publish(nextCount) { this.count = nextCount; for (const widget of [...subscribers]) { widget(this.count); } },};- The store is ready. Header and storage are listening.
When you send a count, the store copies its set and calls 2 listeners.
`cart.subscribe(renderHeader); cart.notify(item);``bus.publish("cart:changed", payload);``target.dispatchEvent(new CustomEvent("cart:changed"));``emitter.once("ready", handler); emitter.emit("ready");``const total = computed(() => price() * quantity());``buttonClicks.map(toPoint).subscribe(draw);`
Sort each line of code by the tool it uses. Observables share a group with signals here, because both are values that make other code run again.
Common misconceptions
- "Every callback is an observer." A callback is just a function you pass to other code. The observer pattern adds a subject that keeps a list of callbacks and calls them each time something changes.
- "Observer and pub/sub are the same." They are close. But pub/sub adds a bus and topic names, so the publisher never knows who is listening.
- "Events always run later." Many don't.
dispatchEventand most simple emitters call every listener right away, before the next line runs. - "Catching errors hides bugs." Catching an error and then ignoring it hides bugs. Catching one listener's error and reporting it keeps the other listeners working.
- "Signals and observables are the same thing." A signal answers "what is the value now?" An observable answers "what values arrive over time?"
- "A too-many-listeners warning means: raise the limit." Usually it means some code forgot to remove its listeners. Look for the missing cleanup first.
The next table lists four common problems. Read each row as: "this can go wrong, this is what happens, and this prevents it."
| What can go wrong | What happens | How to prevent it |
|---|---|---|
| The flow is hidden | Listeners live in other files, so one publish can start work you didn't expect. | Give topics clear names, and keep a list of all topics in one place. |
| A listener is never removed | A part of the page is gone, but its listener still runs and keeps memory in use. | Give back an unsubscribe function and call it. Use AbortController to remove a group at once. |
| One listener throws an error | The error can stop the loop, so later listeners never get the update. | Wrap each call in try/catch, report the error, and keep going through the copied list. |
| One event is too broad | Every listener gets every message and has to check if it cares. | Use narrower topics, or a signal when only one value changed. |
The last table compares the event tools with the value tools. Read across a row to see how each group answers the same question.
| Question | Observer and pub/sub | Signals and observables |
|---|---|---|
| Who keeps the list of listeners? | With observer, the subject keeps it. With pub/sub, the bus keeps it. | Each signal keeps a list of the effects that read it. Each observable subscription keeps its own clean-up step. |
| What does a listener get? | Some data about a thing that just happened. | A signal gives the value now. An observable sends one value after another. |
| What is hard to debug? | Hidden chains of events, and patterns like cart:* that match many topics. | An effect that still listens to an old signal, a missing clean-up, or stream steps far from where you subscribe. |
Practice exercises
5 EXERCISESRead the code. Type the two console lines, with a comma between them.
const observers = [];
function subscribe(observer) {
observers.push(observer);
return () => observers.splice(observers.indexOf(observer), 1);
}
subscribe((value) => console.log("header " + value));
subscribe((value) => console.log("storage " + value));
for (const observer of [...observers]) observer("ready");The header listener subscribed first and storage second. So the two lines are header ready and storage ready.
Which one line prints when the bus emits cart:changed?
const topics = new Map();
function on(topic, handler) {
topics.set(topic, [...(topics.get(topic) ?? []), handler]);
}
function emit(topic, value) {
for (const handler of topics.get(topic) ?? []) handler(value);
}
on("cart:changed", (count) => console.log("header " + count));
on("analytics:cart", (count) => console.log("analytics " + count));
emit("cart:changed", 2);emit uses the topic cart:changed. Only the header listener is on that topic, so it prints header 2.
once listenerPredict the text the two events print.
const target = new EventTarget();
target.addEventListener("save", () => console.log("saved once"), { once: true });
target.dispatchEvent(new Event("save"));
target.dispatchEvent(new Event("save"));The first event prints saved once. The listener then removes itself, so the second event prints nothing.
Type the number the subscriber prints.
let count = 1;
const subscribers = new Set();
const read = () => count;
read.set = (next) => {
count = next;
for (const subscriber of [...subscribers]) subscriber();
};
subscribers.add(() => console.log(read() * 2));
read.set(3);read.set(3) changes count to 3. Then the subscriber prints 3 * 2, which is 6.
A checkout page needs total to update when price or quantity changes. Only the parts of the page that show total should update. Which tool fits best?
const total = computed(() => price() * quantity());A signal fits. total is a value worked out from price and quantity, and only the code that reads total runs again.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which sentence best describes the observer pattern?
Choose an answer to see the explanation.
Question 2 of 7What does this loop over a copy print?
Read the code, then predictconst listeners = []; listeners.push(() => console.log("A")); let stopB = () => {}; stopB = () => listeners.splice(1, 1); listeners.push(() => { console.log("B"); stopB(); }); listeners.push(() => console.log("C")); for (const listener of [...listeners]) listener(); console.log(listeners.length);Choose an answer to see the explanation.
Question 3 of 7What prints from the pub/sub topic example?
Read the code, then predictconst topics = new Map(); function on(topic, handler) { topics.set(topic, [...(topics.get(topic) ?? []), handler]); } function emit(topic, value) { for (const handler of topics.get(topic) ?? []) handler(value); } on("cart:changed", (count) => console.log("header " + count)); on("analytics:cart", (count) => console.log("analytics " + count)); emit("cart:changed", 2);Choose an answer to see the explanation.
Question 4 of 7What does
{ once: true }do here?Read the code, then predictconst target = new EventTarget(); target.addEventListener("save", () => console.log("saved once"), { once: true }); target.dispatchEvent(new Event("save")); target.dispatchEvent(new Event("save"));Choose an answer to see the explanation.
Question 5 of 7Why is the
errorevent special on Node'sEventEmitter?Choose an answer to see the explanation.
Question 6 of 7What does this tiny signal code print?
Read the code, then predictlet count = 1; const subscribers = new Set(); const read = () => count; read.set = (next) => { count = next; for (const subscriber of [...subscribers]) subscriber(); }; subscribers.add(() => console.log(read() * 2)); read.set(3);Choose an answer to see the explanation.
Question 7 of 7What is the most useful difference between signals and observables?
Choose an answer to see the explanation.
Key takeaways
- With observer, the subject keeps the list of listeners and calls them.
- With pub/sub, a bus keeps lists by topic, so the publisher never knows who is listening.
- Copy the list before calling listeners, give back an unsubscribe function, and catch each listener's errors.
- In the browser, use
EventTarget. In Node, useEventEmitter. - A signal is a value you can read now. An observable sends values over time.
Remember the one-liner.
Let code react to a change by deciding who keeps the list of listeners, how messages find them, and when they are removed.
Up next: more design patterns, such as decorator, adapter, and strategy. A later project builds a complete event emitter from scratch.