5 Das (neue) Reactivity-System - dpunkt
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
37 5 Das (neue) Reactivity-System Mit Reactivity wird ein Kernkonzept zusammengefasst, das den Unterschied von modernen Web-Frameworks zu reinem JavaScript oder Bibliotheken wie jQuery verdeutlicht. Es ist der Unterschied zwischen deklarativem und imperativem Ren- dering. In Vue sind die Template-Syntax und das virtuelle DOM zwei Kern- aspekte davon. In jQuery, um bei dem Beispiel zu bleiben, müssen Änderungen an der Oberfläche ganz explizit gesetzt werden. Eine Funktion ändert direkt einen Wert im DOM, fügt eine CSS-Klasse hinzu oder löscht ein Kindelement. Das nachfolgende Code-Snippet zeigt das an einem Beispiel, bei dem eine CSS-Klasse zu DOM-Elementen hinzugefügt wird, wenn ein Button-Klick erfolgt. $('button').click(function() { $('h1, p').addClass('important'); }); Das sind alles direkte, imperative Anweisungen in Form von Kommandos, die das Wie darstellen. Bei der deklarativen Sichtweise moderner Web-Frameworks wer- den Daten verändert, die einen Zustand wiedergeben. Durch diese Zustands- änderungen verändern sich beispielsweise Resultate von Berechnungen, die Inhalte oder die Anzahl von Listenelementen oder sonstige Eigenschaften von View-Ele- menten. Auf Basis dieser Datenänderungen wird anschließend der View Layer an- gepasst. Wir Entwicklerinnen und Entwickler haben damit deklarativ bestimmt, was geschehen soll. Das Wie interessiert uns an dieser Stelle zunächst nicht. Zusammengefasst bedeutet Reactivity, dass sich das Resultat einer Berechnung, wie auch immer diese ausgestaltet ist, ändert, wenn sich die zugrunde liegenden Da- ten ändern. Die veränderten Daten lösen somit einen gewollten Seiteneffekt aus. Das ist immer dann sinnvoll, wenn Daten, Informationen oder eben Resultate von Berechnungen in der Benutzeroberfläche angezeigt werden: zum Beispiel bei einer HTML-Seite in einer Webanwendung. Anders formuliert bietet das Reactivity-Sys- tem einen Mechanismus, die Daten im Modell automatisch synchron mit der Da- tenrepräsentation, dem View-Layer, zu halten. Änderungen an den Daten werden automatisch in der View wiedergegeben, weil entsprechender Code von Vue ausge- führt wird. Das Was (semantisch) ist der bestimmende Faktor, nicht das Wie (tech- nisch). Die Abbildung 5–1 visualisiert diesen Unterschied schematisch.
38 5 Das (neue) Reactivity-System Die Reaktivität von Daten ist ein fundamentales Feature moderner Web-Frame- works. JavaScript als Programmiersprache ist im Standard nicht reaktiv, wie das nachfolgende Beispiel verdeutlicht: let x = 3; let y = 5; let result = x + y; console.log(result); // 8 y = 3; console.log(result); // Weiterhin 8 Die Ausgabe auf der Konsole ist hier vergleichbar mit einem View-Layer, also zum Beispiel der Anzeige in einer HTML-Seite. In modernen Webanwendungen muss sich das Resultat der Berechnung verändern, wenn sich die zugrunde liegen- den Daten ändern, was aber hier nicht passiert. Eine einmal im DOM gerenderte Information, zum Beispiel durch eine Variable, ändert sich nicht auf magische Weise, wenn sich der Inhalt der Variablen ändert. Genau das ist aber eine primäre Anforderung moderner, datengetriebener Webanwendungen. Dieses Zustands- management (State Management) manuell zu implementieren und die Daten mit der View synchron zu halten, ist zwar möglich, bedeutet aber einen erheblichen Aufwand, was Zeit, Nerven und Geld kostet sowie zusätzlich fehleranfällig ist. Abb. 5–1 Schematische Darstellung eines imperativen (oben) und deklarativen (unten) Änderungs- prozesses von Funktionen an einem DOM-Element. Im deklarativen Fall wird das Element indirekt über Änderungen an den Daten verändert.
5.1 Der Ansatz von Vue 39 5.1 Der Ansatz von Vue Das Reactivity-System von Vue ist eines die zentralen Merkmale des Web-Frame- works. Ein Reactivity-System muss primär drei Aufgaben erfüllen, damit es einen Nutzen entfaltet: 1. Es muss erfassen (tracken), wenn ein Wert gelesen wird. Damit wird ermit- telt, welcher Code welche Abhängigkeiten auf Variablen und damit auf Da- ten hat. Beim Ändern von Daten müssen diese Codeteile erneut ausgeführt werden, damit Änderungen an den Daten über die reine Änderung der Vari- ablen hinweg wirksam werden. 2. Das System muss erfassen, wann Werte verändert werden. Denn das ist der Ausgangspunkt für reaktive Veränderungen in anderen Bereichen der An- wendung. 3. Es ist notwendig, dass der Code neu ausgeführt wird, der Daten einer Varia- blen gelesen hat, die anschließend verändert wurde. Das ist der Punkt, in dem sich andere Codeteile einer Anwendung, zum Beispiel Elemente im View-Layer, anpassen und das Reactivity-System auch für uns Entwicklerin- nen und Entwickler sowie für die Nutzerinnen und Nutzer sichtbar wird. Das Reactivity-Modell von Vue wird gerne als unaufdringlich oder unauffällig be- zeichnet. Das bedeutet, es fällt bei der täglichen Entwicklung nicht auf, dass es überhaupt da ist. Es funktioniert einfach. Das Modell einer Vue-Instanz, der Da- tenbereich, besteht aus einfachen JavaScript-Objekten. Die Eigenschaften dieser Objekte enthalten die Daten, und Änderungen daran werden automatisch erfasst, damit Vue das View anpassen kann. Das bedeutet aber auch, dass alle Daten, die reaktiv sein sollen, im Datenbereich einer Vue-Instanz oder einer Komponente de- finiert werden müssen. Fehlt dort zum Beispiel eine Variable, die dann später mit der Textinterpolation gerendert werden soll, warnt Vue, dass die Variable unbe- kannt ist. Ohne die vorherige Definition weiß Vue nichts von der Variablen und konnte diese nicht ins Reactivity-System einbinden und mit den notwendigen An- passungen ausstatten. Wie die dafür nötigen Anpassungen aussehen, beschreibt der nachfolgende Abschnitt 5.2 im Detail auch mit zusätzlichen Informationen für alle, die das Reactivity-System aus Vue 2 kennen. Denn zwischen beiden Versionen bestehen erhebliche Unterschiede. Dieses automatische Reactivity-System hat ei- nige Vorteile. Es spart Zeit bei der Entwicklung, der Code wird einfacher und es reduziert unseren kognitiven Aufwand. Um das Ganze an einem Beispiel festzumachen: In Vue reicht es aus, einen Datenbereich zu erstellen, dort Eigenschaften zu definieren und diese im HTML- Template zu nutzen. Ändern sich die Daten, ändert sich auch die Anzeige im HTML-Template. Das folgende Beispiel zeigt einen Ausschnitt aus der Definition eines Datenbereichs und einer Methode sowie das zugehörige Template.
40 5 Das (neue) Reactivity-System data() { return { message: 'Ich bin ein Test! ', }; }, methods: { onChangeMessage() { this.message = 'Ich bin die neue Nachricht! '; }, }, Reaktive Daten, die sich verändern: {{ message }} Text verändern In diesem Fall registriert Vue, dass es eine Eigenschaft message gibt, die in den Daten definiert ist. Die Methode onChangeMessage greift darauf zu, um sie bei ei- nem Methodenaufruf einmalig auf einen neuen Wert zu verändern. Gebunden werden diese Informationen über das Reactivity-System im Template in einem span-Element, um den Text möglichst einfach anzuzeigen. Abbildung 5–2 zeigt die Veränderung bei einem einfachen Button-Klick. An diesem einfachen Beispiel wird deutlich, wie mächtig das Reactivity-System ist, bei gleichzeitig niedrig- schwelligem Einsatz. Daten definieren, nutzen und verändern, reicht völlig aus. Dabei ist noch der Hinweis relevant, dass der Mausklick, der die Methode auf- ruft, nur ein Beispiel von vielen ist. Durch welchen Mechanismus die Daten ver- ändert werden, kümmert Vue nicht. Der relevante Aspekt ist, dass sie verändert werden und dass alle davon abhängigen Teile der Anwendung auf diese Anpas- sung reagieren. Abb. 5–2 Reaktive Änderungen durch einen Button-Klick Vue bietet drei Möglichkeiten, um Daten in HTML-Templates zu rendern: Pro- pertys, Computed Propertys und Methoden. Näheres dazu verrät das Kapitel 6 zu den Komponenten sowie das Kapitel 7, wenn es um die Verarbeitung von Ein- gabedaten geht. Die Updates des DOM, die aus dem Reactivity-System erfolgen, werden von Vue asynchron durchgeführt. Intern wird eine Queue verwaltet, um alle Ände-
5.2 Die Implementierung als Proxy in Vue 3 41 rungen von Daten in einem Event-Loop zwischenzuspeichern. Das verhindert un- nötige Updates des DOM. Im nächsten Event-Loop-Tick werden die Änderungen dann am DOM durchgeführt. Da die Updates des DOM asynchron erfolgen und von Vue bis zum nächsten Tick zwischengespeichert werden, können wir über nextTick auf den nächsten Flush der DOM-Updates warten: this.$nextTick(() => { // ... }); await this.$nextTick(); Wir können entweder einen Callback als Argument angeben oder mit await auf den Promise warten. 5.2 Die Implementierung als Proxy in Vue 3 Damit das Reactivity-System in Vue funktioniert und um der Tatsache gerecht zu werden, dass es so wenig wie möglich im Weg steht, laufen im Hintergrund von Vue einige Prozesse ab, um aus normalen JavaScript-Objekten reaktive Vue- Objekte zu machen. Dieser Ablauf und die dazu notwendige Implementierung wurden erheblich zwischen den Versionen Vue 3 und 2 angepasst. Für Vue 3 wurde das Reactivity-System als Teil der Modularisierungsstrategie aus dem Vue Core entfernt und als eigenständiges Paket veröffentlicht. Der Code befindet sich wie gewohnt auf GitHub (Team V., @vue/reactivity GitHub Repository, 2022). Vue 3 setzt in der neuen Umsetzung auf Proxy-Objekte. Diese sind notwen- dig, denn JavaScript besitzt im Standard keinen Mechanismus, um mitzubekom- men, ob, geschweige denn wann, eine lokale Variable durch einen neuen Wert überschrieben wurde. Die Zuweisung einer Variablen mit neuen Daten muss Vue aber abfangen, um anschließend darauf reagieren zu können. Daher werden diese Informationen zu Objekten, bei Vue 3 die angesprochenen Proxys, umgewandelt, weil eine Än- derung an Eigenschaften eines Objekts erfasst werden kann. So läuft es in Vue 2 In Vue 2 wurden dazu noch die Daten und Objekte im Data-Bereich einer Komponente durchgegangen, um Getter- und Setter-Methoden für die Daten zu erstellen. Dadurch lassen sich die Zugriffe nachverfolgen.
42 5 Das (neue) Reactivity-System Die Proxy-Implementierung nutzt die neuen Möglichkeiten von ECMAScript 6 (Mozilla, Proxy-Implementierung, 2022). Das dort eingeführte Proxy-Objekt er- laubt es, ein Proxy für ein anderen Objekt zu erzeugen, um dann die darauf aus- geführten Operationen abzufangen. Genau das, was Vue braucht, um einige der ärgerlichen Probleme mit dem Reactivity-System aus Vue 2 auszumerzen – mehr dazu in Abschnitt 5.3. Das Proxy-Objekt ist dafür zuständig, die lesenden und schreibenden Zugriffe auf das originale Objekt abzufangen und zu tracken. Das folgende Snippet zeigt vereinfacht, wie diese Proxys in Vue zum Einsatz kommen: const person = { firstname: 'Fabian', }; const handler = { get(target, property, receiver) { console.log('Lesender Zugriff... '); track(target, property); return Reflect.get(...arguments); }, set(target, property, value, receiver) { console.log('Schreibender Zugriff... '); trigger(target, property); return Reflect.set(...arguments); }, }; const proxy = new Proxy(person, handler); console.log(proxy.firstname); Das originale Objekt heißt in diesem Fall person. Zusätzlich wird ein Handler er- stellt, der dann beim Erzeugen des Proxys zusammen mit dem originalen Objekt übergeben wird. Da der Handler eine get-Methode besitzt, wird diese nun aufge- rufen, wenn die Eigenschaft gelesen wird. Es erfolgt somit die Ausgabe Lesender Zugriff... Fabian auf der Konsole. Vue ist somit in der Lage, festzustellen, wenn die Daten gelesen werden. Durch die set-Methode wird auch das Schreiben neuer Daten über den Proxy erfasst. Vue nutzt das, um die lesenden Zugriffe über einen Aufruf der track-Methode zu tracken und die schreibenden Zugriffe über die trigger- Methode mitzubekommen. Über die track-Methode wird erfasst, dass die Eigen- schaft eine Abhängigkeit zu einem bestimmten Codefragment ist. In Vue wird das ein Effekt genannt. Die trigger-Methode sorgt dafür, dass alle Codeteile erneut ausgeführt werden, wenn sich die Daten verändern. Diese Codeteile sind eben jene, die als abhängiger Code zu einer Variablen erkannt wurden.
5.2 Die Implementierung als Proxy in Vue 3 43 Reflect (Mozilla, Reflect, 2022) sorgt hier dafür, dass sich das Binding von this korrekt verhält. Gewünscht ist, dass alle Methoden an den Proxy gebunden werden und nicht an das originale Zielobjekt. Dadurch ist sichergestellt, dass alle Zugriffe korrekt über den Proxy abgefangen werden können. Abbildung 5–3 zeigt als schematische Darstellung, wie Änderungen nachver- folgt werden, um Änderungen zu triggern. Die Render-Funktion einer Kompo- nente ist vereinfacht gesagt für den Aufbau des virtuellen DOM zuständig. Wenn die Komponente gerendert wird, werden alle Zugriffe auf Getter-Methoden er- fasst, als sogenannter Touch bezeichnet. Diese Zugriffe werden im Watcher, pro Komponente wird ein Watcher erstellt, als Abhängigkeit erfasst. Denn diese per Getter angefragten Daten werden ja für das Rendern der Komponenten benötigt. Durch einen Zugriff auf eine Setter-Methode, wenn Daten verändert werden, lässt sich ein Notify an den Watcher absetzen. Da Daten geändert sind, ist ein neues Rendering erforderlich. Der Watcher weiß, welche Abhängigkeiten es gibt, und kann die betreffenden Codebereiche neu anstoßen und dann das Rendering durchführen. Dieser Vorgang passiert bei jedem Rendering beziehungsweise bei jeder Änderung an den Daten. Abb. 5–3 Schematische Darstellung, wie in Vue Änderungen nachverfolgt werden, um Änderungen zu erfassen und Komponenten neu zu rendern. Das Tracking von Änderungen ist ein Kern des Reactivity-Systems.
44 5 Das (neue) Reactivity-System Damit erfüllt die Proxy-Implementierung die drei Kriterien an Reactivity-Sys- teme aus Abschnitt 5.1. Die Track-Funktion in einer Get-Methode erfasst die ak- tuelle Eigenschaft und den laufenden Effekt. Der Set-Handler bekommt mit, wenn ein Wert verändert wird. Die Trigger-Funktion findet heraus, welche Ef- fekte auf der veränderten Eigenschaft basieren und kann diese erneut ausführen. Reactivity-System in Vue 2 In Version 2 von Vue ist das Reactivity-System komplett anders implementiert. Anstatt Proxys nutzt Vue 2 automatisch generierte Eigenschaften für Getter und Setter. Wenn ein JavaScript-Objekt in einer Vue-Instanz im Data-Bereich genutzt wird, geht Vue das Objekt durch und konvertiert alle Eigenschaften zu Getter und Setter (mit Object.defineProperty). Diese neuen Getter und Setter sind zwar für den Entwickler zunächst nicht sichtbar, werden aber unter der Haube für alle Änderungen an den Eigen- schaften genutzt. Dadurch lassen sich beispielsweise Events auslösen, die über Änderun- gen informieren. Diese Implementierung ist der Grund, warum Vue 2 den IE8 und niedri- ger von Anfang an nicht unterstützt hat, denn dieses Feature kann nicht durch einen Patch (shim) nachgerüstet werden. 5.3 Probleme bei der Reaktivität In Vue 3 kann die Performance beim Umwandeln normaler JavaScript-Objekte zu Proxy-Objekten zu einem Sorgenkind werden. Weil diese Umwandlungen re- kursiv durchgeführt werden. Es betrifft somit auch alle verschachtelten Objekte. Das kann bei einer umfassenden Objektstruktur stark auf die Laufzeit schlagen. Insbesondere, wenn sich Objekte von Drittanbieter-Komponenten in dieser Ob- jektstruktur befinden, da diese Objekte wiederum an sich bereits umfangreich sein können. Daher ist es wichtig, darauf zu achten, ob die Umwandlung mit Proxy-Objekten wirklich über die gesamte Objektstruktur hinweg notwendig ist. Über die Mechanismen von markRaw und den shallow-Funktionen besteht die Mög- lichkeit, selektiv aus dieser tiefen Umwandlung zu reaktiven oder Read-only-Ob- jekten auszusteigen, also diese nicht durchzuführen. Im Zustandsgraphen einer Vue-Instanz lassen sich damit normale JavaScript-Objekte einbinden.
5.3 Probleme bei der Reaktivität 45 Vue 2 und reaktive Objekte In Vue 2 gab es zudem Probleme mit Objekten, wenn dort dynamisch Eigenschaften hin- zugefügt oder entfernt wurden. Da die Kapselung mit Gettern und Settern nur bei der Definition des Objekts geschieht, werden nachträgliche Änderungen nicht erfasst. Bei einem Objekt, bei dem vorher keine Eigenschaft mit dem Namen b existierte, lässt sich daher über die folgende Zeile keine Eigenschaft hinzufügen, die Vue über das Reactivity- System überwacht. const einObject.b = 12; Bei Arrays gibt es ebenfalls Probleme, wenn Elemente direkt über den Index gesetzt wer- den oder die Länge des Arrays angepasst wird: this.elements[10] = 99; this.elements.length = 1; In beiden Fällen bekommt Vue 2 diese Änderungen nicht mit. In allen Fällen lässt sich die Vue.set-Methode nutzen, um Vue direkt mitzuteilen, dass Änderungen an den Objekten beziehungsweise Arrays vorliegen. Beispielsweise mit der folgenden Zeile Code: Vue.set(vm.elements, indexOfItem, newValue); Das funktioniert für das Beispiel mit dem Objekt und dem Array. Alternativ steht der Alias vm.$set zur Verfügung. Mit Vue 3 gehören diese Probleme aber erfreulicherweise alle der Vergangenheit an. Ein weiteres Problem kann bei umfangreichen Objekten entstehen, die durch Vue 3 automatisch in Proxys umgewandelt werden. Dabei werden die Objekte deep reactive. Das bedeutet, dass die verschachtelten Daten ebenfalls reaktiv sind. Dies kann auch aus Versehen passieren, wenn wir ein Objekt in ein anderes einfügen und das eingefügte Objekt dadurch reaktiv machen. Über die Funktion markRaw markieren wir ein Objekt so, dass es niemals reaktiv wird.
Sie können auch lesen