An der UphillConf besuchten Angela Stempfel und ich dieses Jahr nicht die Konferenz sondern einen eintägigen Workshop, der uns eine Einführung in RxJS gab. Dieses Framework verwenden wir bereits in einem Kundenprojekt und mit diesem Workshop wollten wir unser Wissen vertiefen.
Folgende Themen standen auf dem Programm des Workshops:
- Rx Observable, Observer, Subscription
- Converting Things to Observables
- Basic Operators
- Debugging Techniques
- Subjects and Multicasting
Zu jedem der obenstehenden Themen werden wir in diesem Blogpost kurz einen Einblick geben:
Rx Observable, Observer, Subscription
Observable und Observer sind Interfaces, die folgende Methoden zur Verfügung stellen:
Observable Observer - subscribe - next - error - complete
Im Gegensatz zu einer Function, die einen Output sofort liefert, gibt ein Observable mehrere Outputs asynchron zurück. Das Observable stellt den Producer dar. Ein Observer startet mittels subscribe
eine Ausführung, die mehrere Outputs asynchron liefert. Er ist der Consumer.
Allgemein kann gesagt werden, dass das Observable der Producer ist, der Werte generiert und der Observer der Consumer, der diese konsumiert.
Dies sind die 2 wichtigsten Konzepte in RxJs.
Folgendes Beispiel wurde anhand einer Übung von André gezeigt:
function fromArray(arr) { return new Observable(function subscribe(subscriber) { arr.forEach((x) => { subscriber.next(x); }); subscriber.complete(); }); } var observable = fromArray([10,20,30]); observable.subscribe({ next: x => { console.log(x); }, error: e => { console.log(e); } });
Es wird ein neues Observable erstellt, das alle Werte eines Arrays zum Observer liefert. Wichtig ist einerseits das subscriber.complete()
, damit der Observer mitkriegt, wann die Verarbeitung abgeschlossen ist, sowie das subscribe
, damit der Code auch ausgeführt wird.
Observable Grammatik
Die Grammatik des Observables wird mit folgender Regular Expression definiert:
next*(error|complete){0,1}
Die obenstehende Regular Expression beschreibt, wie sich ein Observable verhalten sollte. Sie stellt den „Contract“ auf. Auf kein oder mehrere next
folgt kein oder ein error
oder complete
. Nach einem error
oder complete
wird nichts mehr ausgeliefert.
Subscription
Eine Subscription ist ein Objekt, das die Beziehung zwischen Observer und Observable repräsentiert. Die Subscription hat eine wichtige Methode unsubscribe
, die diese Beziehung auch wieder auflöst. Das heisst, dass die Ausführung des Observables für diesen Observer beendet wird.
import { interval } from 'rxjs'; const observable = interval(1000); const subscription = observable.subscribe(x => console.log(x)); // Later: // This cancels the ongoing Observable execution which // was started by calling subscribe with an Observer. subscription.unsubscribe();
Converting Things to Observables
Es gibt verschiedene Arten, ein Observable zu erstellen. Folgendes Beispiel erstellt ein Observable, das ein „hi“ an jeden Subscriber schickt:
import { Observable } from 'rxjs'; const observable = new Observable(function subscribe(subscriber) { const id = setInterval(() => { subscriber.next('hi') }, 1000); });
Observables können somit mit new Observable
erstellt werden. Meistens werden diese aber mit den zur Verfügung gestellten Funktionen of
, from
, interval
usw. erstellt. Dazu einige Beispiele:
- Erstellung eines Observables von einem Event:
let observable = fromEvent(document, 'click').pipe(take(3));
- Erstellung eines Observables, das in einem Intervall von 1s aufsteigende Zahlenwerte zurückliefert.
const observable = interval(1000).pipe(take(5)); // output: 0,1,2,3,4
- Erstellung eines Observables mit der Funktion
of
const initialWeight$ = of(weightSliderElem.value);
Basic Operators
Operatoren sind Funktionen, die auf ein bestehendes Observable angewendet werden und dabei ein neues Observable auf Basis des bestehenden erstellen. Das bestehende Observable bleibt unverändert!
Auf dem Kurs-Programm standen folgende Gruppen von Operators:
- Transformation
- Filtering
- Combination
- Multicast
- Flattening
- Misc
Auf alle Gruppen von Operators einzugehen, würde den Rahmen dieses Blogposts sprengen. Hier deshalb ein Beispiel aus dem Kurs mit 2 Operators aus der Gruppe Filtering und 1 Operator aus der Gruppe Transformation:
/** * Exercise: from interval(1000), make an Observable with 5 EVEN numbers * multiplied by 100. Then, subscribe to it and show the values * in console.log. */ const observable = interval(1000).pipe( filter(x => x % 2 === 0), take(5), map(x => x * 100), ); observable.subscribe({ next: e => console.log(e), error: e => console.log(':-('), complete: () => console.log('done') });
filter
filtert Werte und nimmt in obigem Beispiel jeweils nur gerade Nummern. take
beschränkt die Verarbeitung auf die ersten 5 Werte. map
transformiert die Werte und multipliziert sie mit 100. Das ergibt folgenden Output:
0 200 400 600 800 done
Marble Diagrams
Damit ersichtlich wird, wie sich Observables verhalten und wie sich Operators auf die Ausführung auswirken, werden sogenannte Marble Diagrams eingesetzt. Diese helfen enorm für das Verständnis, was wie abläuft.
Obiges Beispiel lässt sich wie folgt darstellen:
--0--1--2--3--4--5--6--7--8--9--10--11--12--13--14--> filter(x => x % 2 === 0) --0-----2-----4-----6-----8-----10------12------14--> take(5) --0-----2-----4-----6-----8--|----------------------> map(x => x * 100) --0-----200---400---600---800|---------------------->
Eine Zeile stellt jeweils die Werte eines Observables dar, dann folgt der Operator und auf der nächsten Zeile sieht man die Auswirkung des Operators. Da ein Operator ein neues Observable erstellt, kann dieses wiederum als Input für den nächsten Operator verwendet werden usw.
Debugging Techniques
Wenn mehrere Operators verknüpft werden und beim Output Fehler auftreten, gibt es eine einfache Möglichkeit, ein wenig Licht ins Dunkel zu bringen. Der tap
Operator kann an mehreren Punkten in der Verknüpfung eingefügt werden. Er bietet die Möglichkeit, beispielsweise ein Console Log Statement zu platzieren und den aktuellen Wert der Verarbeitung auszugeben.
Beispiel:
const observable = interval(1000).pipe( tap(x => { console.log('\tfiltering: ' + x) }), filter(x => x % 2 === 0), tap(x => { console.log('\ttaking: ' + x) }), take(5), tap(x => { console.log('\tbefore map: ' + x) }), map(x => x * 100) ); observable.subscribe({ next: e => console.log(e), error: e => console.log(':-('), complete: () => console.log('done') });
Der Output sieht wie folgt aus:
filtering: 0 taking: 0 before map: 0 0 filtering: 1 filtering: 2 taking: 2 before map: 2 200 filtering: 3 filtering: 4 taking: 4 before map: 4 400 filtering: 5 filtering: 6 taking: 6 before map: 6 600 filtering: 7 filtering: 8 taking: 8 before map: 8 800 done
Subjects and Multicasting
Ein Subject ist ebenfalls ein Observable, das man subscriben kann. Es bietet aber die Möglichkeit, Werte per Multicast an mehrere Observers zu liefern. Aus Sicht eines Observers kann nicht unterschieden werden, ob die Werte aus einem normalen unicast Observable kommen oder von einem Subject.
Wenn sich ein Observer bei einem Subject subscribed, wird im Gegensatz zu einem unicast Observable keine neue Execution gestartet. Der Observer reiht sich einfach in die Liste der Observer ein und erhält von diesem Moment an ebenfalls Werte. Hier spricht man etwa auch von einem „Hot subscribe“ im Gegensatz zum „Cold subscribe“ beim Unicast.
Beispiel:
var subject = new Rx.Subject(); subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); subject.next(1); subject.next(2); Output: observerA: 1 observerB: 1 observerA: 2 observerB: 2
Ein Subject gibt einem die Möglichkeit, aus einem unicast Observable ein multicast Observable zu machen, indem man das Subject beim Subscriben als Argument übergibt.
Beispiel:
var subject = new Rx.Subject(); subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); var observable = Rx.Observable.from([1, 2, 3]); observable.subscribe(subject); // You can subscribe providing a Subject Output: observerA: 1 observerB: 1 observerA: 2 observerB: 2 observerA: 3 observerB: 3
Fazit
André Staltz hat uns auf interessante, einfach verständliche und sympatische Art RxJS näher gebracht. Dank einer guten Mischung aus Theorie und selbständigen Übungen war der Tag auf dem Berner Hausberg sehr kurzweilig. Der Workshop hat uns geholfen, den Code unseres Frontend-Spezialisten besser zu verstehen.