RxJS Workshop mit André Staltz

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.

Kommentare sind geschlossen.