Lose gekoppelte Event-Systeme
Das Observer Pattern entkoppelt einen Zustandsträger von den Codeteilen, die auf dessen Änderungen reagieren müssen, ganz ohne direkte Referenzen zwischeneinander. Von einer minimalen Subject-Klasse über das native EventTarget-Interface bis zu WeakRef-basiertem Cleanup zeigt dieser Artikel, wie das Observer Pattern in reinem JavaScript robust implementiert wird.
Inhaltsverzeichnis
- 1. Was das Observer Pattern wirklich löst
- 2. Ein minimales Observer Pattern von Grund auf
- 3. EventTarget: das native Observer Pattern des Browsers
- 4. EventEmitter-Pattern in Vanilla JavaScript nachbauen
- 5. Priorisierte und gefilterte Benachrichtigungen
- 6. Asynchrone Observer mit Promises und Async Iterables
- 7. Speicherverwaltung: WeakRef gegen Observer-Lecks
- 8. Observer Pattern gegenüber Pub/Sub und RxJS
- 9. Observer-Implementierungen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was das Observer Pattern wirklich löst
Das Observer Pattern beschreibt eine Eins-zu-viele-Beziehung zwischen einem Subject, das einen Zustand hält, und beliebig vielen Observern, die über Änderungen dieses Zustands informiert werden wollen, ohne dass Subject und Observer direkte Referenzen aufeinander benötigen. Das Subject kennt nur eine Liste registrierter Callback-Funktionen, nicht die konkrete Identität oder Implementierung der Observer, ein zentrales Merkmal loser Kopplung.
Diese Entkopplung löst ein wiederkehrendes Architekturproblem: ohne Observer Pattern müsste jeder Codeteil, der eine Zustandsänderung auslöst, explizit alle abhängigen Codeteile kennen und direkt aufrufen, was zu einem stark verflochtenen Abhängigkeitsgraphen führt. Mit dem Observer Pattern registriert sich jeder interessierte Codeteil selbstständig, das Subject muss nichts über seine Observer wissen außer der Tatsache, dass sie benachrichtigt werden wollen.
Dieser Artikel implementiert das Observer Pattern in mehreren Varianten: zunächst von Grund auf mit einer minimalen Subject-Klasse, danach über das native EventTarget-Interface des Browsers, als nachgebauter Node.js-Style-EventEmitter, und schließlich mit asynchronen Observern sowie WeakRef-basiertem Speicherschutz.
2. Ein minimales Observer Pattern von Grund auf
Die einfachste Implementierung des Observer Pattern besteht aus einer Subject-Klasse mit drei Methoden: subscribe() registriert einen Observer in einer internen Liste, unsubscribe() entfernt ihn wieder, und notify() ruft jeden registrierten Observer mit den aktuellen Daten auf. Diese drei Methoden bilden das komplette Vokabular des Musters, unabhängig davon, wie komplex die konkrete Anwendung später wird.
Ein wichtiges Detail bei der Implementierung: subscribe() sollte eine Unsubscribe-Funktion zurückgeben, statt den Aufrufer zu zwingen, sich die ursprüngliche Callback-Referenz separat zu merken. Dieses Muster, bekannt aus RxJS und vielen modernen Event-Bibliotheken, macht Cleanup-Code deutlich robuster, weil der Aufrufer nur die zurückgegebene Funktion aufrufen muss, ohne die interne Datenstruktur des Observer Pattern zu kennen.
// Minimal Observer Pattern: Subject with subscribe/unsubscribe/notify
class Subject {
#observers = new Set();
subscribe(observerFn) {
this.#observers.add(observerFn);
// Return an unsubscribe function, the caller never touches the Set directly
return () => this.#observers.delete(observerFn);
}
notify(data) {
for (const observerFn of this.#observers) {
observerFn(data);
}
}
}
const priceSubject = new Subject();
const unsubscribe = priceSubject.subscribe((price) => {
console.log(`Price updated: ${price}`);
});
priceSubject.notify(19.99); // "Price updated: 19.99"
unsubscribe(); // stops receiving further notifications
3. EventTarget: das native Observer Pattern des Browsers
Statt eine eigene Subject-Klasse zu schreiben, kann jede Klasse das native EventTarget-Interface erweitern und damit direkt vom eingebauten Observer Pattern des Browsers profitieren. addEventListener(), removeEventListener() und dispatchEvent() übernehmen exakt die Rolle von subscribe(), unsubscribe() und notify(), mit dem Vorteil, dass diese APIs bereits jedem JavaScript-Entwickler vertraut sind und in DevTools inspizierbar sind.
EventTarget unterstützt zudem AbortSignal als eleganten Weg, mehrere Listener gebündelt zu entfernen: statt jeden einzelnen Listener manuell mit removeEventListener() abzumelden, bricht ein einziger AbortController.abort()-Aufruf alle daran gebundenen Listener gleichzeitig ab. Diese eingebaute Unterstützung macht EventTarget zur pragmatischsten Wahl für das Observer Pattern, wann immer Code ohnehin in einer Browser-Umgebung läuft.
// EventTarget as the browser's native Observer Pattern implementation
class PriceTracker extends EventTarget {
#price = 0;
set price(value) {
this.#price = value;
this.dispatchEvent(new CustomEvent('price-change', { detail: value }));
}
get price() {
return this.#price;
}
}
const tracker = new PriceTracker();
const controller = new AbortController();
tracker.addEventListener('price-change', (event) => {
console.log(`New price: ${event.detail}`);
}, { signal: controller.signal });
tracker.price = 29.99; // triggers the listener
controller.abort(); // removes all listeners bound to this signal at once
4. EventEmitter-Pattern in Vanilla JavaScript nachbauen
Node.js prägt seit Jahren ein eigenes Observer Pattern, den EventEmitter, mit benannten Events statt einem einzigen generischen Notify-Aufruf. Der zentrale Unterschied zur einfachen Subject-Klasse: ein EventEmitter verwaltet eine Map von Event-Namen zu Listener-Arrays, sodass on('save', handler) und on('delete', handler) unabhängig voneinander funktionieren, ohne dass jeder Observer alle Events filtern muss.
Ein selbstgebauter EventEmitter in Vanilla JavaScript funktioniert außerhalb von Node.js überall dort, wo EventTarget aus Kompatibilitätsgründen nicht infrage kommt, etwa in Worker-Threads mit eingeschränkter DOM-API oder in isolierten Testumgebungen. Das Muster bleibt exakt dasselbe Observer Pattern, lediglich die Datenstruktur wechselt von einem einzelnen Set zu einer Map mehrerer Sets, organisiert nach Event-Namen.
// Node.js-style EventEmitter rebuilt in plain JavaScript
class EventEmitter {
#listeners = new Map();
on(eventName, handler) {
if (!this.#listeners.has(eventName)) {
this.#listeners.set(eventName, new Set());
}
this.#listeners.get(eventName).add(handler);
return () => this.#listeners.get(eventName)?.delete(handler);
}
emit(eventName, payload) {
this.#listeners.get(eventName)?.forEach((handler) => handler(payload));
}
}
const orderEvents = new EventEmitter();
orderEvents.on('save', (order) => console.log(`Saved order ${order.id}`));
orderEvents.on('delete', (order) => console.log(`Deleted order ${order.id}`));
orderEvents.emit('save', { id: 42 }); // only "save" listeners run
5. Priorisierte und gefilterte Benachrichtigungen
In größeren Anwendungen reicht eine einfache Liste von Observern oft nicht aus, weil manche Observer vor anderen ausgeführt werden müssen, etwa ein Validierungs-Observer vor einem Logging-Observer. Eine Erweiterung des Observer Pattern speichert jeden Observer zusammen mit einer Priorität und sortiert die interne Liste bei jeder Registrierung neu, sodass notify() Observer in der gewünschten Reihenfolge aufruft, statt in Registrierungsreihenfolge.
Gefilterte Benachrichtigungen sind eine weitere praktische Erweiterung: statt jeden Observer bei jeder Änderung aufzurufen, prüft das Subject vor dem Aufruf einen optionalen Filter-Prädikat, das der Observer bei der Registrierung mitgibt. Das Observer Pattern bleibt dabei strukturell unverändert, nur die notify()-Methode bekommt eine zusätzliche Filterlogik vor dem eigentlichen Aufruf, was unnötige Benachrichtigungen vermeidet, ohne die Registrierungs-API zu verkomplizieren.
6. Asynchrone Observer mit Promises und Async Iterables
Synchrone Observer reichen nicht, sobald ein Observer selbst eine asynchrone Operation ausführen muss, etwa das Schreiben in eine Datenbank nach einer Zustandsänderung. Eine asynchrone Variante des Observer Pattern lässt notify() alle Observer-Promises sammeln und mit Promise.allSettled() auf deren Abschluss warten, statt Observer-Fehler unkontrolliert zu verschlucken oder das Subject blockierend zu machen.
Eine elegante Alternative für Streaming-Szenarien ist ein Async Iterable als Observer-Schnittstelle: statt Callback-Funktionen registriert der Konsument nichts, sondern iteriert direkt mit for await...of über eine Queue, die das Subject intern befüllt. Dieses Muster verbindet das Observer Pattern mit der Iterator-Protokoll-Welt von JavaScript und eignet sich besonders für Datenströme mit Backpressure-Anforderungen.
// Async observer: waiting for all observer promises, error-isolated
class AsyncSubject {
#observers = new Set();
subscribe(observerFn) {
this.#observers.add(observerFn);
return () => this.#observers.delete(observerFn);
}
async notify(data) {
const results = await Promise.allSettled(
[...this.#observers].map((fn) => fn(data))
);
// Isolate failures: one rejected observer must not break the others
results
.filter((r) => r.status === 'rejected')
.forEach((r) => console.error('Observer failed:', r.reason));
}
}
const saveSubject = new AsyncSubject();
saveSubject.subscribe(async (order) => {
await fetch('/api/audit-log', { method: 'POST', body: JSON.stringify(order) });
});
await saveSubject.notify({ id: 42 });
7. Speicherverwaltung: WeakRef gegen Observer-Lecks
Ein häufiges Speicherleck bei jeder Observer Pattern-Implementierung entsteht, wenn Observer sich nie abmelden, etwa weil eine UI-Komponente entfernt wird, ohne unsubscribe() aufzurufen. Das langlebige Subject hält dann eine starke Referenz auf den Observer-Callback und damit indirekt auf alle darin eingeschlossenen Objekte fest, selbst wenn die eigentliche Komponente längst nicht mehr sichtbar ist.
WeakRef in Kombination mit FinalizationRegistry bietet eine Absicherung gegen genau dieses Leck: das Subject speichert Observer als WeakRef, sodass die Garbage Collection den Observer trotzdem einsammeln kann, wenn keine andere starke Referenz mehr existiert. Bei notify() prüft das Subject weakRef.deref(), entfernt bereits eingesammelte Referenzen automatisch aus der Liste und ruft nur noch lebendige Observer auf. Dieses Muster ist kein Ersatz für explizites unsubscribe(), aber ein sinnvolles Sicherheitsnetz bei Observer-Listen mit unsicherer Lebensdauer.
// WeakRef-based observers as a safety net against forgotten unsubscribe calls
class WeakSubject {
#observers = new Set(); // holds WeakRef instances
subscribe(observerFn) {
const ref = new WeakRef(observerFn);
this.#observers.add(ref);
return () => this.#observers.delete(ref);
}
notify(data) {
for (const ref of this.#observers) {
const observerFn = ref.deref();
if (observerFn) {
observerFn(data);
} else {
this.#observers.delete(ref); // garbage collected, clean up the ref
}
}
}
}
8. Observer Pattern gegenüber Pub/Sub und RxJS
Das Observer Pattern und Pub/Sub werden oft synonym verwendet, unterscheiden sich strukturell aber in einem Detail: beim klassischen Observer Pattern kennt das Subject seine Observer direkt, bei echtem Pub/Sub existiert ein vermittelnder Message-Broker zwischen Publisher und Subscriber, sodass Publisher und Subscriber sich nie direkt kennen, selbst nicht über eine gemeinsame Subject-Instanz. Diese zusätzliche Indirektionsebene erlaubt es, Publisher und Subscriber in komplett getrennten Modulen oder sogar Prozessen zu betreiben.
RxJS-Observables erweitern das Observer Pattern um eine reichhaltige Operator-Bibliothek, mit der Ströme von Werten transformiert, kombiniert und gefiltert werden können, etwa debounceTime, merge oder retry. Diese Mächtigkeit hat ihren Preis in Form einer deutlich höheren Lernkurve und zusätzlicher Bundle-Größe, weshalb ein einfaches, selbstgebautes Observer Pattern für viele Anwendungsfälle die pragmatischere Wahl bleibt, ohne auf eine vollständige Reactive-Extensions-Bibliothek angewiesen zu sein.
9. Observer-Implementierungen im Vergleich
Die Wahl zwischen den vorgestellten Varianten des Observer Pattern hängt vom Kontext ab: Browser-Code profitiert meist von EventTarget, komplexe asynchrone Datenströme von RxJS, und einfache, framework-unabhängige Fälle von einer selbstgebauten Subject-Klasse.
| Implementierung | Abhängigkeit | Cleanup | Bester Einsatz |
|---|---|---|---|
| Eigene Subject-Klasse | Keine | Manuell über Unsubscribe-Funktion | Framework-unabhängige Bibliotheken |
| EventTarget | Nativer Browser | AbortController gebündelt | Browser-Code, DOM-nahe Komponenten |
| EventEmitter-Nachbau | Keine | Manuell pro Event-Namen | Node.js-Style-APIs außerhalb Node.js |
| RxJS Observable | Externe Bibliothek | Subscription.unsubscribe() | Komplexe Datenströme mit Operatoren |
In der Praxis lohnt sich eine selbstgebaute Observer Pattern-Implementierung besonders dann, wenn eine Bibliothek framework-unabhängig bleiben soll, ohne die Bundle-Größe einer vollständigen Reactive-Extensions-Bibliothek zu tragen. Die vorgestellten Bausteine, von der minimalen Subject-Klasse bis zum WeakRef-basierten Cleanup, lassen sich beliebig kombinieren, je nach Anforderung an Speicherverwaltung und asynchrones Verhalten.
Mironsoft
Event-Architektur, Entkopplung und Speicherverwaltung im Frontend
Event-Systeme ohne Speicherlecks und feste Kopplung?
Wir analysieren bestehende Event-Architekturen, ersetzen eng gekoppelten Code durch sauber implementiertes Observer Pattern und sichern Cleanup mit AbortController oder WeakRef ab.
Event-Audit
Analyse bestehender Listener-Registrierungen auf Speicherlecks
Observer-Refactoring
Umbau eng gekoppelter Callback-Ketten in ein sauberes Observer Pattern
Cleanup-Strategie
AbortController-Integration und WeakRef-basierte Sicherheitsnetze
10. Zusammenfassung
Das Observer Pattern entkoppelt Zustandsträger von den Codeteilen, die auf Änderungen reagieren, über eine einfache Registrierungs- und Benachrichtigungsschnittstelle. Eine minimale Subject-Klasse mit subscribe(), unsubscribe() und notify() genügt für die meisten Anwendungsfälle, während EventTarget dieselbe Funktionalität nativ im Browser bereitstellt, inklusive AbortController-gebündeltem Cleanup.
Fortgeschrittene Varianten wie asynchrone Observer mit Promise.allSettled() und WeakRef-basierte Referenzen gegen Speicherlecks erweitern das Grundmuster für produktionsreife Anforderungen. Verglichen mit Pub/Sub über einen Message-Broker oder RxJS-Observables bleibt ein selbstgebautes Observer Pattern die leichtgewichtigste Option, wann immer die Mächtigkeit einer vollständigen Reactive-Extensions-Bibliothek nicht gebraucht wird.
Observer Pattern in JavaScript — Das Wichtigste auf einen Blick
Grundmuster
Subject mit subscribe(), unsubscribe() und notify(), Rückgabe einer Unsubscribe-Funktion.
Native Alternative
EventTarget mit AbortController für gebündeltes Cleanup, ideal im Browser.
Asynchron & Speicher
Promise.allSettled() für async Observer, WeakRef als Sicherheitsnetz gegen Lecks.
Abgrenzung
Pub/Sub fügt einen Message-Broker hinzu, RxJS erweitert um eine Operator-Bibliothek.