Observer Pattern in JavaScript implementieren: Lose gekoppelte Event-Systeme
AI generated
JS
() =>
JavaScript · Observer Pattern · Design Patterns · Framework agnostisch
Observer Pattern in JavaScript implementieren
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.

18 Min. Lesezeit Subject/Observer · EventTarget · EventEmitter · WeakRef Framework-unabhängig

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.

11. FAQ: Observer Pattern in JavaScript

1Was ist das Observer Pattern?
Eins-zu-viele-Beziehung zwischen Subject und Observern ohne direkte Referenzen aufeinander.
2Warum Unsubscribe-Funktion zurueckgeben?
Erleichtert Cleanup ohne Kenntnis der internen Datenstruktur.
3EventTarget besser als eigene Klasse?
Im Browser meist ja, wegen nativer AbortSignal-Integration.
4Observer Pattern vs. EventEmitter?
EventEmitter ist eine konkrete Auspraegung mit benannten Events statt einem generischen notify.
5Fehler in asynchronen Observern?
Promise.allSettled() isoliert Fehler eines Observers von den uebrigen.
6Wofuer WeakRef?
Sicherheitsnetz gegen Speicherlecks bei vergessenem Unsubscribe.
7Observer Pattern vs. Pub/Sub?
Pub/Sub fuegt einen vermittelnden Message-Broker hinzu.
8Wann RxJS statt eigenem Muster?
Bei komplexen Stroemen mit Operatoren wie debounceTime oder retry.
9Observer priorisieren?
Ja, ueber gespeicherte Prioritaet und sortierte interne Liste.
10Funktioniert es ausserhalb des Browsers?
Ja, eigene Subject-Klasse oder EventEmitter-Nachbau funktionieren ueberall.