Vue.js 3: Reactivity System und Composition API unter der Lupe

Seite 2: Änderungen am Reactivity-System

Inhaltsverzeichnis

Das Reactivity-System gilt schon seit der ersten Version von Vue.js als eines der herausragenden Features des Web-Frameworks. Die Art und Weise wie Reactivity in Vue konzipiert und implementiert ist, gilt als “unobtrusive“, also unauffällig oder unaufdringlich. Das meiste passiert im Hintergrund. Ein Model ist dabei ein JavaScript-Objekt, verpackt in einem Proxy. Dadurch ist es möglich, dass sich bei einer Änderung Teile der Anwendung, wie beispielsweise die Oberfläche, aktualisieren lassen. Diese Art der Implementierung macht es einfach, das Reactivity-System von Vue.js zu nutzen, führt aber gleichzeitig dazu, dass die Funktionsweise bekannt sein muss, um typische Stolperfallen zu vermeiden.

Basis für das Reactivity-System sind die in JavaScript eingebauten Proxy. Genutzt wird ein Handler mit Getter- und Setter-Methoden. Dieses ES6-Feature bietet schlankere Proxies und eine erhöhte Performance. In Vue.js Version 2 hat diese Aufgabe noch die statische Methode Object.defineProperty übernommen. Vue hat die Methode genutzt, um alle Eigenschaften eines Objekts ebenfalls zu Getter/Setter umzuwandeln. In Version 3 von Vue wird das neue Feature abwärtskompatibel implementiert, ein Fallback zu Object.defineProperty ist vorhanden. So lässt sich auch der Internet Explorer (IE) als Browser weiterhin unterstützen. Das folgende Listing zeigt ein Beispiel für die interne Nutzung eines Proxy mit einem Handler. Zu sehen sind das Tracking mit der track-Funktion und die Benachrichtigung über Änderungen von Werten über die trigger-Funktion:

const person = {
  name: 'Fabian Deitelhoff'
};
const handler = {
  get(target, prop, receiver) {
    track(target, prop)
    return Reflect.get(...arguments)
  },
  set(target, key, value, receiver) {
    trigger(target, key)
    return Reflect.set(...arguments)
  }
};
const proxy = new Proxy(person, handler);
console.log(proxy.name);

Für Vue.js 3 wird das Dependency-Tracking und die Change-Notification als separates Paket angeboten. Dadurch lassen sich die Features als Standalone-Variante nutzen. Ein kleiner Nachteil ergibt sich aus dieser Änderung, da jetzt die Ausgabe von console.log die Daten anders formatiert, wenn Datenobjekte ausgegeben werden sollen. Es ist daher ratsam, die Vue Devtools zu nutzen. Abbildung 2 zeigt, wie umfangreich die Ausgabe durch einen Proxy wird.

Ausgabe eines Proxy-Objekts auf der Konsole des Chrome-Browsers (Abb. 2)

Zu beachten ist, dass Vue.js intern alle Objekte trackt, die mit dem Schlüsselwort reactive deklariert sind. Das gleiche Objekt liefert daher den identischen Proxy zurück. Geschachtelte Objekte sind vor der Rückgabe ebenfalls als reactive Proxies gekapselt. Das Proxy-Objekt ist jedoch nicht mit dem originalen Objekt identisch, daher schlägt der Vergleichsoperator === fehl. In den meisten Fällen verhalten sich beide Objekt-Varianten gleich. Funktionen wie filter oder map, die auf Identitätsvergleichen basieren, schlagen aber fehl. Bei der Arbeit mit der Composition API ist es gelebte Praxis, keine Referenz auf das originale Objekt vorzuhalten, sondern nur mit der reaktiven Version zu arbeiten, um diesen problematischen Fällen aus dem Weg zu gehen.

Online-Konferenz: Vue Day 2021 am 15. Juni

(Bild: Shutterstock)

In Kooperation mit Vuejs.de, der deutschen Vue-Community, richten dpunkt.verlag und heise developer am 15. Juni 2021 den Vue Day 2021 aus. Die eintägige Online-Konferenz richtet sich sowohl an Einsteiger als auch an fortgeschrittene Vue-Kenner.

Programm-Highlights:

  • Vue von 0 auf 100
  • Horizonterweiterung: Vue 3 ohne die Options API oder Vuex dank Composables
  • Ein Blick hinter die Kulissen des Webframeworks
  • Stabilere Vue Applikationen mit Jest und der Testing Library
  • Bloggen mit Nuxt-Content
  • Ab auf die Insel: die Island-Architecture mit Vue und Eleventy

Nähere Informationen zur Online-Konferenz sowie zu den flankierenden Workshops rund um das Webframework bietet die Webseite der Veranstaltung.

Hier gilt es zunächst, die veränderten Möglichkeiten bei der Definition von reaktiveren Zuständen – reactive states – zu erläutern. Um von einem JavaScript-Objekt eine reaktive Version zu erzeugen, ist die Methode reactive notwendig. Da bei Vue.js 3 jetzt alles in Module, also in unterschiedliche Pakete, aufgeteilt ist, gilt es diese Methode zunächst zu importieren. Anschließend lässt sich ein reactive-Objekt erzeugen:

import { reactive } from 'vue';
const state = reactive({
  counter: 0
});

Das reactive ist hier identisch zum Vue.observable aus Vue.js 2.x. Der primäre Anwendungsfall dieser tiefen reaktiveren Kopie ist der Einsatz während des Render-Prozesses. Das bereits erwähnte Dependency-Tracking sorgt dafür, dass sich die Oberfläche anpasst, wenn sich der reaktive Zustand ändert. Die neuen Möglichkeiten des Reactivity-Systems merzen etliche Probleme von Vue.js 2 aus, die zum Beispiel in der Dokumentation im Abschnitt zu den “Change Detection Caveats“ beschrieben sind. Unter anderem lassen sich Objekte und Arrays ohne Umstände tracken.

Neben der reactive-Methode existieren noch weitere für verschiedene Anwendungsfälle. Zum Beispiel readonly, um einen unveränderbaren Proxy zurückzuliefern, der ebenfalls eine sogenannte Deep Copy ist – das heißt, dass alle Eigenschaften und Daten eines Objekts in die Kopie übernommen werden. Zusätzliche Methoden wie isProxy und isReactive prüfen, ob es sich um ein reactive- oder readonly-Proxy handelt, oder ob es sich ganz explizit um einen Proxy handelt, der mittels reactive-Methode erstellt wurde. isReadonly prüft respektive, ob es sich um einen Proxy handelt, der mit readonly erzeugt wurde.

Weitere Funktionen erläutert die Dokumentation zur dritten Hauptversion des Webframeworks Vue.js im Abschnitt über die Basic Reactivity APIs.

Um die Arbeit mit den Reactivity States und den Proxy-Objekten zu vereinfachen, bietet Vue.js 3 einige Optionen an. Sie helfen insbesondere im Zusammenspiel mit der neuen Composition API. Für sich alleinstehende Werte, beispielsweise eine Zahl oder eine Zeichenkette, lassen sich zwar in einem einzelnen Objekt kapseln und mit der reactive-Methode zu einem Proxy transformieren. Allerdings verursacht das auf Dauer unnötigen Mehraufwand. Zum gleichen Ergebnis führt die Methode ref, vereinfacht aber die Deklaration:

import { ref } from 'vue';
const counter = ref(0);

Das Beispiel ist identisch zu der Definition von counter im Codeausschnitt weiter oben – nur deutlich schlanker und für den täglichen Gebrauch einsetzbar. Der Name stammt von “Reference“, da das zurückgegebene, reaktive und veränderbare Objekt als reaktive Referenz auf den internen Wert dient. Die einzige Eigenschaft dieses Objekts heißt value und liefert den Inhalt zurück. Kommt diese Art Referenz im Render-Kontext zum Einsatz – damit ist das von der Setup-Methode zurückgelieferte Objekt gemeint – wird bei einem Zugriff automatisch der gespeicherte Wert verwendet. In einem Template ist daher kein Zugriff über count.value notwendig, um beim vorherigen Beispiel zu bleiben. In einem Vue-Template ist der Zugriff über value nur notwendig, wenn es sich um verschachtelte refs handelt.

Dieses sogenannte Ref Unwrapping findet auch beim Zugriff über ein reaktives Objekt statt, wenn die Referenz als Eigenschaft Verwendung findet:

const counter = ref(0);
const state = reactive({
  counter
});
state.counter = 1;
console.log(state.counter);

Beim Zuweisen eines ref zu einer bereits bestehenden Referenz, beispielsweise einer Objekt-Eigenschaft, wird die vorhandene Referenz überschrieben. Zu beachten ist, dass das automatische Auspacken nur bei refs passiert, die in einem reaktiven Objekt verschachtelt sind. Beim Zugriff über Arrays oder Maps ist die Nutzung der Eigenschaft value notwendig.

Beim sogenannten Destructuring von Eigenschaften eines Objekts geht die Reaktivität für die so herausgelösten Eigenschaften verloren. Das reaktive Objekt gilt es zunächst über toRefs in eine Sammlung von refs umzuwandeln:

import { reactive, toRefs } from 'vue';
const customer = reactive({
    id: 1,
    name: 'Customer 1',
    balance: 1000,
});
let { id, name } = toRefs(customer);

Bei den Computed Properties und Watchern gab es ebenfalls zahlreiche Änderungen in Vue.js 3, die alle mit den Anpassungen am Reactivity-System zusammenhängen. Mit der Methode computed lässt sich direkt ein computed value erzeugen, die ein unveränderbares ref-Objekt zurückliefert:

const addOne = computed(() => counter.value + 1);

Wenn das zurückgelieferte Objekt schreibbar sein soll, kann alternativ die Definition mit einer get- und einer set-Methode erfolgen:

const plusOne = computed({
  get: () => counter.value + 1,
  set: val => {
    counter.value = val - 1
  }
});

Über die neue Methode watchEffect lässt sich ein Watcher definieren, der automatisch einen Seiteneffekt ausführt, wenn sich der beobachtete Wert ändert. Im folgenden Beispiel ist das die Ausgabe auf der Konsole, allerdings sind auch andere Fälle denkbar:

const count = ref(0);
const watcher = watchEffect(() => console.log(counter.value));
setTimeout(() => {
  counter.value++
}, 100);

Für das Stoppen des Watcher ist die Rückgabe zu nutzen: watcher(). Die Rückgabe sollte sinnvollerweise stop oder ähnlich heißen. Zu den Möglichkeiten der Referenzen gibt es in der Vue.js-3-Dokumentation einen eigenen Abschnitt, der viele weitere Funktionen beschreibt. Darüber hinaus befassen sich mehrere Abschnitte der Dokumentation mit dem manuellen Stoppen, möglichen Seiteneffekten, dem Flush Timing und Debuggen.

Die zuvor genannten Anpassungen und Neuerungen in Vue.js lassen sich umfassend in der neuen Composition API verwenden. Der Knackpunkt des neuen Ansatzes ist, wiederholbare beziehungsweise wiederverwendbare Teile aus einer Komponente herauszulösen. Vue-Anwendung sollen dadurch deutlich wartbarer und flexibler werden, weil sich diese Code-Fragmente, die jeweils eine einzelne Funktionalität implementieren, fast beliebig miteinander verbinden lassen. Bei kleineren Webanwendungen mag sich dieser Unterschied noch nicht bemerkbar machen. Steigt die Anzahl der Vue-Komponenten jedoch, ist der positive Effekt umso größer.

Die logische Organisation von Komponenten mit den Komponenten-Optionen (Options API) data, computed, methods, watch und dergleichen funktioniert zwar in der Regel, wird aber problematischer, wenn die Komponente im Umfang wächst. Die Verantwortlichkeiten der einzelnen Teile einer Komponente vermischen sich zu stark. Dieses Problem besteht auch in TypeScript mit dem Class-Based-Ansatz. Dort lassen sich zwar Methoden etwas besser zu logischen und funktionalen Gruppen zusammenfassen, allerdings bleibt zumindest die Wiederverwendbarkeit ein Problem.