Teil 1. Reaktivität und Flüsse
Diese Artikelserie konzentriert sich auf die Reaktivität und ihre Anwendung in JS unter Verwendung einer so wunderbaren Bibliothek wie RxJS.
Artikelserie "Grundlagen der reaktiven Programmierung mit RxJS":
Für wen dieser Artikel ist : Im Grunde werde ich hier die Grundlagen erklären, daher ist der Artikel in erster Linie für Anfänger in dieser Technologie gedacht. Gleichzeitig hoffe ich, dass erfahrene Entwickler etwas Neues für sich lernen können. Das Verständnis erfordert Kenntnisse über js (es5 / es6).
Motivation : Ich bin RxJS zum ersten Mal begegnet, als ich anfing, mit Angular zu arbeiten. Damals hatte ich Schwierigkeiten, den Reaktivitätsmechanismus zu verstehen. Die Tatsache, dass zu Beginn meiner Arbeit die meisten Artikel der alten Version der Bibliothek gewidmet waren, trug zu den Schwierigkeiten bei. Ich musste viel Dokumentation lesen, verschiedene Handbücher, um zumindest etwas zu verstehen. Und erst nach einiger Zeit wurde mir klar, wie „alles arrangiert ist“. Um anderen das Leben zu erleichtern, habe ich beschlossen, alles in die Regale zu stellen.
Was ist Reaktivität?
Es ist schwierig, eine Antwort auf einen scheinbar gebräuchlichen Begriff zu finden. Kurz gesagt: Reaktivität ist die Fähigkeit, auf Änderungen zu reagieren. Aber über welche Veränderungen sprechen wir? Zunächst zu Datenänderungen. Betrachten Sie ein Beispiel:
let a = 2; let b = 3; let sum = a + b; console.log(sum);
Dieses Beispiel zeigt das bekannte imperative Programmierparadigma. Im Gegensatz zum imperativen Ansatz baut der reaktive Ansatz auf Push-Change-Propagationsstrategien auf. Die Push-Strategie impliziert, dass bei Datenänderungen dieselben Änderungen „durchgesetzt“ werden und die von ihnen abhängigen Daten automatisch aktualisiert werden. So würde sich unser Beispiel verhalten, wenn eine Push-Strategie angewendet würde:
let a = 2; let b = 3; let sum = a + b; console.log(sum);
Dieses Beispiel zeigt einen reaktiven Ansatz. Es ist erwähnenswert, dass dieses Beispiel nichts mit der Realität zu tun hat. Ich habe es nur gegeben, um den Unterschied in den Ansätzen zu zeigen. Reaktiver Code in realen Anwendungen sieht ganz anders aus, und bevor wir mit dem Üben fortfahren, sollten wir über eine weitere wichtige Komponente der Reaktivität sprechen.
Datenstrom
Wenn Sie sich den Begriff „reaktive Programmierung“ in Wikipedia ansehen, erhalten Sie auf der Website die folgende Definition: „Reaktive Programmierung ist ein Programmierparadigma, das sich auf Datenflüsse und die Verbreitung von Änderungen konzentriert.“ Aus dieser Definition können wir schließen, dass die Reaktivität auf zwei Hauptwalen basiert. Ich habe oben die Verteilung der Änderungen erwähnt, daher werden wir nicht weiter darauf eingehen. Wir sollten jedoch mehr über Datenströme sprechen. Schauen wir uns das folgende Beispiel an:
const input = document.querySelector('input');
Wir hören uns das Keyup-Ereignis an und fügen das Ereignisobjekt in unser Array ein. Im Laufe der Zeit kann unser Array Tausende von KeyboardEvent-Objekten enthalten. Es ist erwähnenswert, dass unser Array nach Zeit sortiert ist - der Index späterer Ereignisse ist größer als der Index früherer Ereignisse. Ein solches Array ist ein vereinfachtes Datenflussmodell. Warum vereinfacht? Weil das Array nur Daten speichern kann. Wir können das Array auch iterieren und seine Elemente irgendwie verarbeiten. Das Array kann uns jedoch nicht mitteilen, dass ein neues Element hinzugefügt wurde. Um herauszufinden, ob dem Array neue Daten hinzugefügt wurden, müssen wir sie erneut durchlaufen.
Aber was wäre, wenn unser Array uns darüber informieren könnte, dass neue Daten darin angekommen sind? Ein solches Array könnte mit Sicherheit als Stream bezeichnet werden. Wir kommen also zur Definition des Flusses. Ein Stream ist ein nach Zeit sortiertes Datenarray, das anzeigen kann, dass sich die Daten geändert haben.
Beobachtbar
Nachdem wir nun wissen, was Flüsse sind, arbeiten wir mit ihnen. In RxJS werden Streams durch die Observable-Klasse dargestellt. Um einen eigenen Stream zu erstellen, rufen Sie einfach den Konstruktor dieser Klasse auf und übergeben Sie die Abonnementfunktion als Argument:
const observable = new Observable(observer => { observer.next(1); observer.next(2); observer.complete(); })
Durch Aufrufen des Konstruktors der Observable-Klasse erstellen wir einen neuen Thread. Als Argument haben wir die Abonnementfunktion an den Konstruktor übergeben. Die Abonnementfunktion ist eine reguläre Funktion, die einen Beobachter als Parameter verwendet. Der Betrachter selbst ist ein Objekt, das drei Methoden hat:
- next - wirft einen neuen Wert in den Stream
- error - wirft einen Fehler in den Stream, woraufhin der Stream endet
- complete - beendet den Thread
Also haben wir einen Thread erstellt, der zwei Werte ausgibt und beendet.
Abonnement
Wenn wir den vorherigen Code ausführen, passiert nichts. Wir werden nur einen neuen Stream erstellen und den Link dazu in der beobachtbaren Variablen speichern, aber der Stream selbst wird niemals einen einzigen Wert ausgeben. Dies liegt daran, dass Threads „faule“ Objekte sind und nichts für sich tun. Damit unser Stream Werte ausgibt und wir diese Werte verarbeiten können, müssen wir den Stream „abhören“. Sie können dies tun, indem Sie die subscribe-Methode für das beobachtbare Objekt aufrufen.
const observer = { next: value => console.log(value),
Wir identifizierten unseren Beobachter und beschrieben drei Methoden für ihn: als nächstes Fehler, vollständig. Methoden protokollieren einfach Daten, die als Parameter übergeben werden. Dann rufen wir die Subscribe-Methode auf und übergeben unseren Beobachter an sie. Im Moment des Aufrufs von subscribe wird die Subscription-Funktion aufgerufen, die wir beim Deklarieren unseres Streams an den Konstruktor übergeben haben. Als nächstes wird der Code der Abonnementfunktion ausgeführt, der zwei Werte an unseren Beobachter übergibt und dann den Stream beendet.
Sicherlich haben viele eine Frage, was passiert, wenn wir den Stream erneut abonnieren? Alles wird gleich sein: Der Stream übergibt wieder zwei Werte an den Beobachter und endet. Bei jedem Aufruf der Subscribe-Methode wird eine Subscription-Funktion aufgerufen und der gesamte Code erneut ausgeführt. Daraus können wir schließen: Unabhängig davon, wie oft wir den Stream abonnieren, erhalten unsere Beobachter dieselben Daten.
Abbestellen
Versuchen wir nun, ein komplexeres Beispiel zu implementieren. Wir werden einen Timer schreiben, der Sekunden ab dem Zeitpunkt des Abonnements zählt und diese an Beobachter weiterleitet.
const timer = new Observable(observer => { let counter = 0;
Der Code ist recht einfach. Innerhalb der Abonnementfunktion deklarieren wir eine Zählervariable. Dann schließen wir mit Closure über die Pfeilfunktion in setInterval auf die Variable. Und jede Sekunde übergeben wir die Variable an den Beobachter, danach erhöhen wir sie. Abonnieren Sie als Nächstes den Stream und geben Sie nur eine Methode an - next. Machen Sie sich keine Sorgen, dass wir keine anderen Methoden angekündigt haben. Keine der Beobachtermethoden ist erforderlich. Wir können sogar ein leeres Objekt übergeben, aber in diesem Fall wird der Thread verschwendet.
Nach dem Start sehen wir die begehrten Protokolle, die jede Sekunde erscheinen. Wenn Sie möchten, können Sie den Stream mehrmals experimentieren und abonnieren. Sie werden sehen, dass jeder der Threads unabhängig von den anderen ausgeführt wird.
Wenn Sie darüber nachdenken, wird unser Thread während der gesamten Lebensdauer der gesamten Anwendung ausgeführt, da wir keine Logik zum Abbrechen von setInterval haben und in der Abonnementfunktion die vollständige Methode nicht aufgerufen wird. Aber was ist, wenn wir den Thread brauchen, um zu enden?
In der Tat ist alles sehr einfach. Wenn Sie sich die Dokumentation ansehen, sehen Sie, dass die Subscribe-Methode ein Subscription-Objekt zurückgibt. Dieses Objekt verfügt über eine Abmeldemethode. Wir nennen es und unser Beobachter wird keine Werte mehr vom Stream empfangen.
const subscription = timer.subscribe({next: console.log}); setTimeout(() => subscription.unsubscribe(), 5000);
Nach dem Start sehen wir, dass der Zähler bei 4 stoppt. Obwohl wir uns vom Stream abgemeldet haben, funktioniert unsere setInterval-Funktion weiterhin. Sie erhöht unseren Zähler jede Sekunde und gibt ihn an den Dummy-Beobachter weiter. Um dies zu verhindern, müssen Sie die Logik zum Abbrechen des Intervalls schreiben. Dazu müssen Sie eine neue Funktion aus der Abonnementfunktion zurückgeben, in der die Kündigungslogik implementiert wird.
const timer = new Observable(observer => { let counter = 0; const intervalId = setInterval(() => { observer.next(counter++); }, 1000); return () => { clearInterval(intervalId); } });
Jetzt können wir aufatmen. Nach dem Aufrufen der Methode zum Abbestellen wird unsere Funktion zum Abbestellen aufgerufen, mit der das Intervall gelöscht wird.
Fazit
Dieser Artikel zeigt die Unterschiede zwischen imperativen und reaktiven Ansätzen sowie Beispiele für die Erstellung eigener Flows. Im nächsten Teil werde ich darüber sprechen, welche anderen Methoden zum Erstellen von Threads existieren und wie sie angewendet werden.