Die Mauer des Lifecycle-Managements
Eine der größten Fallen von RxJS ist das Lifecycle-Management. Wenn Sie "wann sollte ich subscriben" und "wann sollte ich unsubscriben" falsch machen, kann dies zu Speicherlecks und Bugs führen.
Wann sollte man subscribe?
Grundprinzip: Subscribe nicht bis zum allerletzten Moment
❌ Schlechtes Beispiel: Subscribe in der Mitte
import { interval } from 'rxjs';
function getEvenNumbers() {
const numbers$ = interval(1000);
// Subscribe hier drin
numbers$.subscribe(n => {
if (n % 2 === 0) {
console.log(n); // Wie übergebe ich dies nach außen?
}
});
}✅ Gutes Beispiel: Observable zurückgeben und auf der aufrufenden Seite subscriben
import { interval } from 'rxjs';
import { filter, take } from 'rxjs';
function getEvenNumbers() {
return interval(1000).pipe(
filter(n => n % 2 === 0),
take(5)
);
}
// Subscribe auf der Verwendungsseite
const subscription = getEvenNumbers().subscribe(n => {
console.log(n);
});💡 Erklärung
- Schlechtes Beispiel: Wenn Sie innerhalb einer Funktion subscriben, verlieren Sie die Kontrolle (kann nicht abgebrochen, nicht komponiert werden)
- Gutes Beispiel: Durch Rückgabe eines Observable kann die aufrufende Seite die Kontrolle übernehmen
Subscribe ist ein Trigger für "Nebeneffekte"
❌ Schlechtes Beispiel: Mehrere Nebeneffekte innerhalb von subscribe ausführen
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';
const button = document.querySelector('button')!;
fromEvent(button, 'click')
.pipe(map(() => Math.random()))
.subscribe(randomValue => {
// Nebeneffekt 1: DOM-Manipulation
document.querySelector('#result')!.textContent = randomValue.toString();
// Nebeneffekt 2: API-Aufruf
fetch('/api/log', {
method: 'POST',
body: JSON.stringify({ value: randomValue })
});
// Nebeneffekt 3: Lokaler Speicher
localStorage.setItem('lastValue', randomValue.toString());
});✅ Gutes Beispiel: Nebeneffekte trennen und nur das Notwendige subscriben
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';
const button = document.querySelector('button')!;
const randomClicks$ = fromEvent(button, 'click').pipe(
map(() => Math.random())
);
// Nur DOM-Update benötigt
randomClicks$.subscribe(value => {
document.querySelector('#result')!.textContent = value.toString();
});
// Nur Logging benötigt
randomClicks$.subscribe(value => {
fetch('/api/log', {
method: 'POST',
body: JSON.stringify({ value })
});
});💡 Erklärung
- subscribe = Ausführungspunkt für Nebeneffekte
- Wenn Nebeneffekte unabhängig sind: In mehrere subscribes aufteilen (individuell steuerbar)
- Wenn Nebeneffekte immer als Set ausgeführt werden: In einem subscribe zusammenfassen ist OK
- Wenn Nebeneffekte in der Pipeline benötigt werden:
tapOperator verwenden
Wann sollte man subscribe: Entscheidungsflussdiagramm
Gesamtbild des Subscription-Lebenszyklus
Das folgende Zustandsübergangsdiagramm zeigt, welche Zustände ein Observable-Subscription durchläuft, bevor es beendet wird.
Wichtige Punkte des Lifecycle-Managements
- Abonniert: Zustand mit Speicherleck-Gefahr
- complete/error: Automatisches Cleanup (unsubscribe nicht erforderlich)
- unsubscribe: Manuelles Cleanup erforderlich (besonders bei unendlichen Streams)
Wann sollte man unsubscribe?
Grundprinzip: Immer unsubscriben, wenn man abonniert hat
❌ Schlechtes Beispiel: Kein unsubscribe → Speicherleck
import { interval } from 'rxjs';
const button = document.querySelector('button')!;
function startTimer() {
interval(1000).subscribe(n => {
console.log(n);
});
// Dieses Abonnement läuft ewig weiter!
}
// Bei jedem Button-Klick wird ein neues Abonnement hinzugefügt
button.addEventListener('click', startTimer);
// 10 Klicks = 10 Abonnements laufen gleichzeitig!✅ Gutes Beispiel: Mit unsubscribe aufheben
import { interval } from 'rxjs';
function startTimer() {
const subscription = interval(1000).subscribe(n => {
console.log(n);
});
// Nach 5 Sekunden aufheben
setTimeout(() => {
subscription.unsubscribe();
console.log('Abonnement aufgehoben');
}, 5000);
}💡 Erklärung
- Unendliche Streams (interval, fromEvent etc.) benötigen immer unsubscribe
- Ohne unsubscribe gibt es Speicherlecks + unnötige Verarbeitung läuft weiter
Fälle, in denen unsubscribe nicht erforderlich ist
✅ Observable, die automatisch complete
of(1, 2, 3).subscribe(n => console.log(n));
// Nach complete automatisches Cleanup
from([1, 2, 3]).subscribe(n => console.log(n));
// Nach complete automatisches Cleanup✅ Mit take etc. ist Completion garantiert
interval(1000).pipe(
take(5) // Automatisches complete nach 5 Mal
).subscribe(n => console.log(n));✅ Beendigung durch Error
throwError(() => new Error('Error')).subscribe({
error: err => console.error(err)
});✅ EMPTY (sofortiges complete)
EMPTY.subscribe(() => console.log('Wird nicht ausgeführt'));💡 Erklärung
unsubscribe ist nicht erforderlich in folgenden Fällen:
- Observable, die complete() aufrufen - Automatisches Cleanup
- Wenn error() aufgerufen wird - Ebenfalls automatisches Cleanup
- Mit take(n) etc. ist Completion garantiert - Explizites complete
Wichtig
Unendliche Streams (interval, fromEvent, Subject etc.) benötigen immer unsubscribe!
Entscheidungsflussdiagramm: Ist unsubscribe erforderlich?
Im Zweifel unsubscriben ist sicher!
Muster zur Verhinderung von Speicherlecks
Muster 1: Verwaltung mit Subscription-Objekt
import { interval, fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';
class MyComponent {
private subscription = new Subscription();
ngOnInit() {
// Mehrere Abonnements zu einem Subscription hinzufügen
this.subscription.add(
interval(1000).subscribe(n => console.log('Timer:', n))
);
this.subscription.add(
fromEvent(document, 'click').subscribe(() => console.log('Click!'))
);
this.subscription.add(
fromEvent(window, 'resize').subscribe(() => console.log('Resize!'))
);
}
ngOnDestroy() {
// Alle Abonnements auf einmal aufheben
this.subscription.unsubscribe();
}
}💡 Vorteile
- Mehrere Abonnements mit einem Objekt verwalten
- Batch-Aufhebung in
ngOnDestroy - Einfaches Hinzufügen und Entfernen
Muster 2: Verwaltung mit Array
import { interval, fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';
class MyComponent {
private subscriptions: Subscription[] = [];
ngOnInit() {
this.subscriptions.push(
interval(1000).subscribe(n => console.log('Timer:', n))
);
this.subscriptions.push(
fromEvent(document, 'click').subscribe(() => console.log('Click!'))
);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
}
}💡 Vorteile
- Flexible Verwaltung mit Array-Operationen
- Individuelle Aufhebung möglich
- Leichter zu debuggen (Array mit console.log überprüfbar)
Muster 3: takeUntil-Muster (empfohlen)
import { interval, fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
class MyComponent {
private destroy$ = new Subject<void>();
ngOnInit() {
// takeUntil(this.destroy$) zu allen Abonnements hinzufügen
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(n => console.log('Timer:', n));
fromEvent(document, 'click').pipe(
takeUntil(this.destroy$)
).subscribe(() => console.log('Click!'));
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$)
).subscribe(() => console.log('Resize!'));
}
ngOnDestroy() {
// Alle Abonnements mit einem next() aufheben
this.destroy$.next();
this.destroy$.complete();
}
}💡 Vorteile
- Am deklarativsten - Completion-Bedingung in der Pipeline explizit
- Subscription-Objekt nicht erforderlich - Speichereffizient
- Leicht lesbar - Beim Lesen des Codes wird klar "complete mit destroy$"
Vollständiger Leitfaden zum takeUntil-Muster
Basismuster
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
const destroy$ = new Subject<void>();
// Dieses Abonnement läuft bis destroy$ next() aufruft
interval(1000).pipe(
takeUntil(destroy$)
).subscribe(n => console.log(n));
// Alle Abonnements nach 5 Sekunden stoppen
setTimeout(() => {
destroy$.next();
destroy$.complete();
}, 5000);Marble Diagram
interval(1000): --0--1--2--3--4--5--6--7-->
destroy$: ----------X
↑
next() Aufruf
takeUntil-Ergebnis: --0--1--2|
↑
completeAnwendung auf mehrere Observables
import { interval, fromEvent, timer, Subject } from 'rxjs';
import { takeUntil, map } from 'rxjs';
const destroy$ = new Subject<void>();
// Muster: Dasselbe destroy$ für alle Streams verwenden
interval(1000).pipe(
takeUntil(destroy$),
map(n => `Timer: ${n}`)
).subscribe(console.log);
fromEvent(document, 'click').pipe(
takeUntil(destroy$),
map(() => 'Click!')
).subscribe(console.log);
timer(2000).pipe(
takeUntil(destroy$),
map(() => 'Timer finished')
).subscribe(console.log);
// Batch-Stopp
function cleanup() {
destroy$.next();
destroy$.complete();
}
// Beispiel: cleanup() bei Seitenübergang aufrufen
window.addEventListener('beforeunload', cleanup);Häufige Fehler beim takeUntil-Muster
Fehler 1: takeUntil ist an der falschen Position
❌ Schlechtes Beispiel: map nach takeUntil
import { interval, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs';
const destroy$ = new Subject<void>();
interval(1000).pipe(
takeUntil(destroy$), // Auch wenn hier complete...
map(n => n * 2) // map könnte ausgeführt werden
).subscribe(console.log);✅ Gutes Beispiel: takeUntil zuletzt platzieren
import { interval, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs';
const destroy$ = new Subject<void>();
interval(1000).pipe(
map(n => n * 2),
takeUntil(destroy$) // Nach allen Operatoren
).subscribe(console.log);💡 Erklärung
- takeUntil so weit wie möglich zuletzt platzieren
- Ausnahme: Vor Multicast-Operatoren wie shareReplay kann es auch platziert werden
Fehler 2: destroy$ nicht complete
❌ Schlechtes Beispiel: complete() nicht aufgerufen
import { Subject } from 'rxjs';
const destroy$ = new Subject<void>();
function cleanup() {
destroy$.next();
// ❌ complete() nicht aufgerufen
}
// Problem: destroy$ selbst wird zur Ursache von Speicherlecks✅ Gutes Beispiel: Sowohl next() als auch complete() aufrufen
import { Subject } from 'rxjs';
const destroy$ = new Subject<void>();
function cleanup() {
destroy$.next();
destroy$.complete();
}💡 Erklärung
- Nur
next()lässt destroy$ selbst abonniert - Immer auch
complete()aufrufen
Fehler 3: Versuch der Wiederverwendung
❌ Schlechtes Beispiel: Wiederverwendung eines completed Subject
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
const destroy$ = new Subject<void>();
function start() {
interval(1000).pipe(
takeUntil(destroy$)
).subscribe(console.log);
}
function stop() {
destroy$.next();
destroy$.complete();
}
start();
setTimeout(stop, 3000);
// ❌ Problem: destroy$ ist bereits complete, also wird start() sofort beendet
setTimeout(start, 5000); // Das funktioniert nicht✅ Gutes Beispiel: destroy$ neu erstellen
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
class MyComponent {
private destroy$ = new Subject<void>();
start() {
// Falls bereits complete, neu erstellen
if (this.destroy$.closed) {
this.destroy$ = new Subject<void>();
}
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(console.log);
}
stop() {
this.destroy$.next();
this.destroy$.complete();
}
}💡 Erklärung
- Subject kann nach complete nicht wiederverwendet werden
- Bei Bedarf ein neues Subject erstellen
Best Practices für Subscription-Management
Best Practice 1: destroy$ pro Komponente/Klasse haben
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
class UserProfileComponent {
private destroy$ = new Subject<void>();
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUser().pipe(
takeUntil(this.destroy$)
).subscribe(user => {
console.log(user);
});
this.userService.getUserPosts().pipe(
takeUntil(this.destroy$)
).subscribe(posts => {
console.log(posts);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}💡 Vorteile
- Konsistenz - Gleiches Muster in allen Komponenten
- Wartbarkeit - Keine Änderung von ngOnDestroy beim Hinzufügen neuer Abonnements erforderlich
- Sicherheit - Kein Vergessen von unsubscribe
Best Practice 2: AsyncPipe nutzen (im Fall von Angular)
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-user-profile',
template: `
<!-- AsyncPipe subscribt und unsubscribt automatisch -->
<div *ngIf="user$ | async as user">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
<ul>
<li *ngFor="let post of posts$ | async">
{{ post.title }}
</li>
</ul>
`
})
export class UserProfileComponent {
user$: Observable<User>;
posts$: Observable<Post[]>;
constructor(private userService: UserService) {
// Observable direkt an Template übergeben
this.user$ = this.userService.getUser();
this.posts$ = this.userService.getUserPosts();
// ngOnDestroy nicht erforderlich! AsyncPipe hebt automatisch auf
}
}💡 Vorteile
- Automatisches unsubscribe - Automatische Aufhebung bei Komponenten-Zerstörung
- OnPush-kompatibel - Optimierte Change Detection
- Prägnanter Code - Kein subscribe/unsubscribe Boilerplate erforderlich
Best Practice 3: Strategie nach langlebig vs. kurzlebig ändern
import { Injectable } from '@angular/core';
import { BehaviorSubject, interval, fromEvent } from 'rxjs';
import { takeUntil } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DataService {
// ✅ Über gesamten Service geteilter Zustand (langlebig)
// → Abonnement bis Anwendungsende beibehalten
private userState$ = new BehaviorSubject<User | null>(null);
getUser() {
return this.userState$.asObservable();
}
// ❌ Nicht direkt in Komponente subscriben
// ✅ Mit AsyncPipe oder takeUntil subscriben
}
class MyComponent {
private destroy$ = new Subject<void>();
ngOnInit() {
// ✅ An Komponenten-Lebenszyklus gebundenes Abonnement (kurzlebig)
// → Unbedingt in ngOnDestroy aufheben
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(n => console.log(n));
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$)
).subscribe(() => console.log('Resize'));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}💡 Prinzip
| Abonnementtyp | Lebenszyklus | Verwaltungsmethode |
|---|---|---|
| Globaler Zustand | Gesamte Anwendung | BehaviorSubject + AsyncPipe |
| Seiten-/Route-spezifisch | Während Route gültig | takeUntil(routeDestroy$) |
| Komponenten-spezifisch | Während Komponente existiert | takeUntil(destroy$) oder AsyncPipe |
| Einmaliger API-Aufruf | Bis zur Completion | take(1) oder first() |
Best Practice 4: Explizite Completion-Bedingung festlegen
❌ Schlechtes Beispiel: Unklar, wann es endet
import { fromEvent } from 'rxjs';
fromEvent(document, 'click').subscribe(() => {
console.log('Click');
});✅ Gutes Beispiel 1: Anzahlbegrenzung
import { fromEvent } from 'rxjs';
import { take } from 'rxjs';
fromEvent(document, 'click').pipe(
take(5) // Automatisches Ende nach 5 Mal
).subscribe(() => {
console.log('Click (maximal 5 Mal)');
});✅ Gutes Beispiel 2: Zeitbegrenzung
import { fromEvent, timer } from 'rxjs';
import { takeUntil } from 'rxjs';
const timeout$ = timer(10000); // Nach 10 Sekunden
fromEvent(document, 'click').pipe(
takeUntil(timeout$)
).subscribe(() => {
console.log('Click (innerhalb von 10 Sekunden)');
});✅ Gutes Beispiel 3: Mehrere Endbedingungen
import { fromEvent, Subject, merge } from 'rxjs';
import { takeUntil, take } from 'rxjs';
const destroy$ = new Subject<void>();
const maxClicks$ = fromEvent(document, 'click').pipe(take(10));
fromEvent(document, 'mousemove').pipe(
takeUntil(merge(destroy$, maxClicks$)) // Ende bei einem von beiden
).subscribe(() => {
console.log('Mouse move');
});💡 Prinzip
- "Wann endet es" explizit machen - Unendliche Streams vermeiden
- Completion-Bedingung mit take, first, takeWhile, takeUntil etc. festlegen
- An Lebenszyklus binden (destroy$, timeout$, etc.)
Verständnis-Checkliste
Prüfen Sie, ob Sie die folgenden Fragen beantworten können.
## Grundverständnis
- [ ] Kann erklären, was passiert, wenn man ein Observable subscribt
- [ ] Kann unterscheiden, wann unsubscribe erforderlich ist und wann nicht
- [ ] Kann die Ursachen von Speicherlecks erklären
## Muster-Anwendung
- [ ] Kann mehrere Abonnements mit Subscription-Objekt verwalten
- [ ] Kann takeUntil-Muster implementieren
- [ ] Kann destroy$ richtig platzieren (letzter Operator)
## Best Practices
- [ ] Weiß, wann AsyncPipe verwendet werden sollte
- [ ] Kann langlebige und kurzlebige Abonnements unterscheiden und verwalten
- [ ] Kann explizite Completion-Bedingungen festlegen
## Debugging
- [ ] Kennt Methoden zur Erkennung von Speicherlecks
- [ ] Kann vergessene unsubscribes finden
- [ ] Kann Abonnementzahl mit Browser DevTools überprüfenNächste Schritte
Nachdem Sie das Lifecycle-Management verstanden haben, lernen Sie als Nächstes die Operator-Auswahl.
→ Das Dilemma der Operator-Auswahl - Kriterien zur Auswahl des richtigen Operators aus über 100
Verwandte Seiten
- Kapitel 2: Lebenszyklus von Observable - Grundlagen von subscribe/unsubscribe
- Kapitel 10: Häufige Fehler und Gegenmaßnahmen - Verschachtelte subscribes, Speicherlecks etc.
- Kapitel 13: Formularverarbeitungsmuster - Praktische Anwendung (in Vorbereitung)
- Kapitel 8: Debugging von Speicherlecks - Debugging-Methoden
🎯 Übungsaufgaben
Aufgabe 1: Speicherleck beheben
Der folgende Code hat ein Speicherleck. Beheben Sie es.
class ChatComponent {
ngOnInit() {
interval(5000).subscribe(() => {
this.chatService.checkNewMessages().subscribe(messages => {
console.log('Neue Nachrichten:', messages);
});
});
}
}Lösungsbeispiel
class ChatComponent {
private destroy$ = new Subject<void>();
ngOnInit() {
interval(5000).pipe(
takeUntil(this.destroy$),
switchMap(() => this.chatService.checkNewMessages())
).subscribe(messages => {
console.log('Neue Nachrichten:', messages);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Verbesserungen
destroy$Subject hinzugefügttakeUntil(this.destroy$)stoppt interval- Verschachteltes subscribe mit
switchMapaufgelöst - Cleanup in
ngOnDestroy
Aufgabe 2: Geeignetes Muster auswählen
Wählen Sie das optimale Abonnement-Verwaltungsmuster für die folgenden Szenarien.
- HTTP-Request (nur einmal)
- WebSocket-Verbindung (während Komponente existiert)
- Globaler Benutzerzustand (gesamte Anwendung)
Lösungsbeispiel
1. HTTP-Request (nur einmal)
// ✅ take(1) oder first() - Automatisches complete nach einmal
this.http.get('/api/user').pipe(
take(1)
).subscribe(user => console.log(user));
// Oder AsyncPipe (im Fall von Angular)
user$ = this.http.get('/api/user');2. WebSocket-Verbindung (während Komponente existiert)
// ✅ takeUntil-Muster - Trennung bei Komponenten-Zerstörung
private destroy$ = new Subject<void>();
ngOnInit() {
this.websocket.connect().pipe(
takeUntil(this.destroy$)
).subscribe(message => console.log(message));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}3. Globaler Benutzerzustand (gesamte Anwendung)
// ✅ BehaviorSubject + AsyncPipe - unsubscribe nicht erforderlich
@Injectable({ providedIn: 'root' })
class AuthService {
private userState$ = new BehaviorSubject<User | null>(null);
getUser() {
return this.userState$.asObservable();
}
}
// Verwendung in Komponente
user$ = this.authService.getUser(); // Subscribe mit AsyncPipe