RxJS ist eine beliebte Javascript Bibliothek für reaktive Programmierung. Die Grundlage darin bilden Observables, doch erst dank den Operators wird es möglich, komplexen asynchronen Code deklarativ zu programmieren. Sie beinhaltet eine Menge von vorgefertigten Operators – so viele, dass man leicht den Überblick verlieren kann.
Dieser Blogeintrag vergleicht die beliebten Higher-Order Mapping Operators concatMap, exhaustMap, mergeMap und switchMap. Mittels Code-Beispielen wird auf die Unterschiede und ihren Einfluss auf die Datentransformation eingegangen. Zunächst wird der Aufbau der Beispiele beschrieben, danach werden ihre Resultate erklärt. Am Schluss gibt es eine kleine Zusammenfassung der Erkenntnisse.
Jedes der nachfolgenden Beispiele verändert bzw. transformiert ein Observable anhand eines der oben genannten Operatoren und gibt das Resultat aus. Das Observable wird einer simplen Multiplikation unterzogen. Bei jeder Transformation wird eine Verzögerung von höchstens drei Sekunden eingebaut. Dadurch kann die Netzwerklatenz simuliert werden. Um den Vergleich zwischen den Operatoren zu ermöglichen, sind alle Code-Beispiele ähnlich aufgebaut, wobei der Aufbau auf dem switchMap-Beispiel der RxJS-Dokumentation basiert [1].
console.log("insert name of operator here"); const numberObservable = of(1, 2); const resultOfOperator = numberObservable.pipe( <<insert operator here>>((x) => of(x * 5).pipe(delay(1000 * Math.floor(Math.random() * 3))) ) ); resultOfOperator.subscribe((x) => console.log('r ' + x));
Das Ergebnis der angewandten Transformationen ist in Abbildung 1 zu finden.
Unter der Verwendung von Html, Typescript und RxJS werden nachfolgend die Operatoren und deren Rolle bei der Datenverarbeitung erläutert.
In diesem Abschnitt werden die Resultate beschrieben.
In Abbildung 1 ist erkennbar, dass der concatMap-Operator die Werte des äusseren Observable (im Code «numberObservable») in sequenzieller Reihenfolge verarbeitet. Ursache dafür ist das innere Observable (unten im Code hervorgehoben). Es muss fertig sein, bevor concatMap weitere Daten des «numberObservable» verarbeiten kann [2].
console.log('concatMap'); const numberObservable = of(1, 2); const concatMapResult = numberObservable.pipe( concatMap((x) => of(x * 5).pipe(delay(1000 * Math.floor(Math.random() * 3))) //inneres Observable ) ); concatMapResult.subscribe((x) => console.log('c ' + x));
Auch hier werden die vorhandenen Daten mit fünf multipliziert. In Abbildung 1 ist deutlich, dass nach der Ausführung von exhaustMap zwar das Resultat «5» angezeigt wird, jedoch die «10» fehlt. Um dieses Verhalten zu verstehen, muss man die Werte des «numberObservable» als einen Datenstrom betrachten, wie das Marble-Diagramm in Abbildung 2 zeigt. Der Wert, welcher zuerst das innere Observable erreicht, wird auch zuerst transformiert. Der exhaustMap-Operator ignoriert dabei alle anderen eingetroffenen Daten, solange das innere Observable mit der Transformierung nicht fertig ist. In diesem Fall trifft «2» ein, während das innere Observable noch «1» bearbeitet, weshalb dieser Wert ignoriert wird. Daher kann man sagen, dass exhaustMap alle Werte des äusseren Observable ignoriert und nicht verarbeitet, solange das innere Observable noch aktiv ist [3]. Dabei muss betont werden, dass beide Werte verarbeitet und ausgegeben werden, wenn die Werte zeitgleich eintreffen.
console.log('exhaustMap'); const numberObservable = of(1, 2); const exhaustMapResult = numberObservable.pipe( exhaustMap((x) => of(x * 5).pipe(delay(1000 * Math.floor(Math.random() * 3))) //inneres Observable ) ); exhaustMapResult.subscribe((x) => console.log('e ' + x));
Gemäss der Resultate in Abbildung 1 unterscheiden sich mergeMap und concatMap nicht voneinander. Nach mehrfacher Ausführung zeigen die Abbildungen 3 und 4 allerdings ein anderes Bild: ConcatMap gibt verlässlich die Berechnungen in sequenzieller Folge aus, wohingegen der mergeMap-Operator die Resultate in beliebiger Reihenfolge ausgibt. Der Grund für dieses Verhalten liegt darin, dass der mergeMap-Operator nicht auf den Abschluss des inneren Observable wartet und daher den nächsten angekommenen Wert auch verarbeitet [4]. Das heisst, eine zeitliche Überschneidung der Verarbeitungen ist möglich, weshalb die Resultate keine sequenzielle Verarbeitungsreihenfolge aufweisen. Die Marble-Diagramme in Abbildung 5 verdeutlichen dieses Verhalten. Man sieht, dass je nach Timing entweder «5» oder «10» ausgegeben wird.
console.log('mergeMap'); const numberObservable = of(1, 2); const mergeMapResult = numberObservable.pipe( mergeMap((x) => of(x * 5).pipe(delay(1000 * Math.floor(Math.random() * 3))) //inneres Observable ) ); mergeMapResult.subscribe((x) => console.log('m ' + x));
Abbildung 1 lässt erkennen, dass sowohl switchMap als auch exhaustMap nur einen Wert zurückgegeben haben. Dennoch hat switchMap im Unterschied zu exhaustMap nur das Resultat des letzten Werts im Observable ausgegeben. Dies liegt darin, dass sich switchMap nur für den letzten Wert des inneren Observable interessiert. Alle vorherigen Berechnungen werden abgebrochen [1]. Wie bei exhaustMap muss auch hier hervorgehoben werden, dass das Timing wichtig ist. Denn auch hier können «5» und «10» bei zeitnahem Eintreffen der Werte ausgegeben werden.
console.log('switchMap'); const numberObservable = of(1, 2); const switchMapResult = numberObservable.pipe( switchMap((x) => of(x * 5).pipe(delay(1000 * Math.floor(Math.random() * 3))) //inneres Observable ) ); switchMapResult.subscribe((x) => console.log('s ' + x));
Zum Schluss werden die Erkenntnisse der Ausführungen zusammengefasst:
[1] | RxJS, «RxJS – switchMap,» [Online]. Available: https://rxjs.dev/api/operators/switchMap. [Zugriff am 19 November 2023]. |
[2] | RxJS, «RxJS – concatMap,» [Online]. Available: https://rxjs.dev/api/operators/concatMap. [Zugriff am 19 November 2023]. |
[3] | RxJS, «RxJS – exhaustMap,» [Online]. Available: https://rxjs.dev/api/operators/exhaustMap. [Zugriff am 19 November 2023]. |
[4] | RxJS, «RxJS – mergeMap,» [Online]. Available: https://rxjs.dev/api/operators/mergeMap. [Zugriff am 19 November 2023]. |
Schreiben Sie einen Kommentar