Reaktivität in der Tiefe
Eines der markantesten Merkmale von Vue ist das unaufdringliche Reaktivitätssystem. Der Zustand einer Komponente besteht aus reaktiven JavaScript-Objekten. Wenn Sie diese ändern, wird die Ansicht aktualisiert. Es macht die Zustandsverwaltung einfach und intuitiv, aber es ist auch wichtig zu verstehen, wie es funktioniert, um einige häufige Fehler zu vermeiden. In diesem Abschnitt werden wir uns mit einigen Details des Reaktivitätssystems von Vue beschäftigen.
Was ist Reaktivität?
Dieser Begriff taucht in der Programmierung heutzutage recht häufig auf, aber was ist damit gemeint? Reaktivität ist ein Programmierparadigma, mit dem wir uns auf deklarative Weise an Änderungen anpassen können. Das kanonische Beispiel, das die Leute gewöhnlich zeigen, weil es ein großartiges Beispiel ist, ist eine Excel-Tabelle:
A | B | C | |
---|---|---|---|
0 | 1 | ||
1 | 2 | ||
2 | 3 |
In diesem Fall ist die Zelle A2 durch die Formel = A0 + A1
definiert (Sie können auf A2 klicken, um die Formel anzuzeigen oder zu bearbeiten), so dass das Rechenblatt 3 anzeigt. Das ist keine Überraschung. Aber wenn Sie A0 oder A1 aktualisieren, werden Sie feststellen, dass A2 automatisch mit aktualisiert wird.
JavaScript funktioniert normalerweise nicht so. Wenn wir etwas Vergleichbares in JavaScript schreiben würden:
js
let A0 = 1
let A1 = 2
let A2 = A0 + A1
console.log(A2) // 3
A0 = 2
console.log(A2) // Still 3
Wenn wir A0
mutieren, ändert sich A2
nicht automatisch.
Wie würden wir das in JavaScript machen? Um den Code zur Aktualisierung von „A2“ erneut auszuführen, müssen wir ihn zunächst in eine Funktion verpacken:
js
let A2
function update() {
A2 = A0 + A1
}
Dann müssen wir ein paar Begriffe definieren:
Die Funktion
update()
erzeugt einen Seiteneffekt, oder kurz Effekt, weil sie den Zustand des Programms verändert.A0
undA1
werden als Abhängigkeiten des Effekts betrachtet, da ihre Werte zur Ausführung des Effekts verwendet werden. Der Effekt wird als Abonnent für seine Abhängigkeiten bezeichnet.
Was wir brauchen, ist eine magische Funktion, die update()
(die Auswirkung) aufrufen kann, wenn sich A0
oder A1
(die Abhängigkeiten) ändern:
js
whenDepsChange(update)
This whenDepsChange()
Funktion hat folgende Aufgaben:
Verfolgen, wann eine Variable gelesen wird. Wenn z.B. der Ausdruck
A0 + A1
ausgewertet wird, werden sowohlA0
als auchA1
gelesen.Wenn eine Variable gelesen wird, während ein Effekt gerade läuft, mache diesen Effekt zu einem Abonnenten dieser Variable. Da z.B.
A0
undA1
gelesen werden, wennupdate()
ausgeführt wird, wirdupdate()
nach dem ersten Aufruf zu einem Abonnenten vonA0
undA1
.Erkennen, wenn eine Variable verändert wird. Wenn z.B.
A0
ein neuer Wert zugewiesen wird, werden alle seine Abonnenten benachrichtigt, damit sie erneut ausgeführt werden.
Wie Reaktivität in Vue funktioniert
Wir können das Lesen und Schreiben von lokalen Variablen wie im Beispiel nicht wirklich verfolgen. Es gibt einfach keinen Mechanismus, um das in Vanilla JavaScript zu tun. Was wir jedoch können, ist das Abfangen des Lesens und Schreibens von Objekteigenschaften.
Es gibt zwei Möglichkeiten, den Zugriff auf Eigenschaften in JavaScript abzufangen: getter / setters und Proxies. Vue 2 verwendet aufgrund von Einschränkungen der Browserunterstützung ausschließlich Getter / Setter. In Vue 3 werden Proxies für reaktive Objekte und Getter / Setter für Refs verwendet. Hier ist einige Pseudo-Code, der zeigt, wie sie funktionieren:
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
TIP
Die Codeschnipsel hier und weiter unten sollen die Kernkonzepte in möglichst einfacher Form erklären, daher werden viele Details weggelassen und Randfälle ignoriert.
Dies erklärt einige Einschränkungen von reaktiven Objekten, die wir im Abschnitt über die Grundlagen diskutiert haben:
Wenn Sie die Eigenschaft eines reaktiven Objekts einer lokalen Variablen zuweisen oder destrukturieren, wird die Reaktivität „abgekoppelt“, da der Zugriff auf die lokale Variable nicht mehr die Get/Set-Proxy-Fallen auslöst.
Der von
reactive()
zurückgegebene Proxy verhält sich zwar genauso wie das Original, hat aber eine andere Identität, wenn wir ihn mit dem Operator===
mit dem Original vergleichen.
Innerhalb von track()
wird geprüft, ob es einen laufenden Effekt gibt. Wenn es einen gibt, suchen wir nach den Abonnenteneffekten (gespeichert in einem Set) für die Eigenschaft, die verfolgt wird, und fügen den Effekt dem Set hinzu:
js
// Dies wird unmittelbar vor der Ausführung eines Effekts gesetzt
// ausgeführt werden soll. Wir werden uns später damit befassen.
let activeEffect
function track(target, key) {
if (activeEffect) {
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
Effekt-Abonnements werden in einer globalen WeakMap<Ziel, Map<Schlüssel, Set<Effekt>>>
Datenstruktur gespeichert. Wenn für eine Eigenschaft (die zum ersten Mal verfolgt wird) kein Set mit abonnierten Effekten gefunden wurde, wird es erstellt. Das ist es, was die Funktion getSubscribersForProperty()
in Kurzform tut. Der Einfachheit halber werden wir die Details überspringen.
Innerhalb von trigger()
suchen wir wieder nach den Abonnenteneffekten für die Eigenschaft. Aber dieses Mal rufen wir sie stattdessen auf:
js
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
Kehren wir nun zur Funktion whenDepsChange()
zurück:
js
function whenDepsChange(update) {
const effect = () => {
activeEffect = effect
update()
activeEffect = null
}
effect()
}
Sie verpackt die rohe update
-Funktion in einen Effekt, der sich selbst als den aktuell aktiven Effekt setzt, bevor er die eigentliche Aktualisierung durchführt. Dies ermöglicht track()
-Aufrufe während der Aktualisierung, um den aktuellen aktiven Effekt zu finden.
An dieser Stelle haben wir einen Effekt erstellt, der automatisch seine Abhängigkeiten verfolgt und bei jeder Änderung einer Abhängigkeit erneut ausgeführt wird. Wir nennen dies einen Reaktiven Effekt.
Vue bietet eine API, die es erlaubt, reaktive Effekte zu erstellen: watchEffect()
. In der Tat haben Sie vielleicht bemerkt, dass es ziemlich ähnlich wie das magische whenDepsChange()
im Beispiel funktioniert. Wir können nun das ursprüngliche Beispiel mit aktuellen Vue-APIs überarbeiten:
js
import { ref, watchEffect } from 'vue'
const A0 = ref(0)
const A1 = ref(1)
const A2 = ref()
watchEffect(() => {
// Spuren A0 und A1
A2.value = A0.value + A1.value
})
// löst die Wirkung aus
A0.value = 2
Die Verwendung eines reaktiven Effekts zum Ändern einer Referenz ist nicht der interessanteste Anwendungsfall - die Verwendung einer berechneten Eigenschaft macht ihn deklarativer:
js
import { ref, computed } from 'vue'
const A0 = ref(0)
const A1 = ref(1)
const A2 = computed(() => A0.value + A1.value)
A0.value = 2
Intern verwaltet computed
seine Ungültigkeitserklärung und Neuberechnung mit Hilfe eines reaktiven Effekts.
Was ist also ein Beispiel für einen üblichen und nützlichen reaktiven Effekt? Nun, das Aktualisieren des DOM! Wir können ein einfaches „reaktives Rendering“ wie folgt implementieren:
js
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
document.body.innerHTML = `count is: ${count.value}`
})
// aktualisiert das DOM
count.value++
In der Tat kommt dies der Art und Weise, wie eine Vue-Komponente den Zustand und das DOM synchron hält, ziemlich nahe - jede Komponenteninstanz erzeugt einen reaktiven Effekt, um das DOM zu rendern und zu aktualisieren. Natürlich verwenden Vue-Komponenten viel effizientere Wege, um das DOM zu aktualisieren als innerHTML
. Dies wird in Rendering-Mechanismus diskutiert.
Laufzeit vs. Kompilierzeit Reaktivität
Das Reaktivitätssystem von Vue ist in erster Linie laufzeitbasiert: Das Tracking und die Auslösung werden alle durchgeführt, während der Code direkt im Browser läuft. Die Vorteile der Laufzeit Reaktivität sind, dass es ohne einen Build-Schritt arbeiten kann, und es gibt weniger Randfälle. Andererseits ist sie dadurch durch die Syntaxbeschränkungen von JavaScript eingeschränkt.
Wir sind bereits im vorherigen Beispiel auf eine Einschränkung gestoßen: JavaScript bietet uns keine Möglichkeit, das Lesen und Schreiben lokaler Variablen abzufangen, so dass wir immer auf reaktive Zustände als Objekteigenschaften zugreifen müssen, entweder mit reaktiven Objekten oder mit refs.
Wir haben mit der Funktion Reactivity Transform experimentiert, um den Umfang des Codes zu verringern:
js
let A0 = $ref(0)
let A1 = $ref(1)
// Spur auf Variable lesen
const A2 = $computed(() => A0 + A1)
// Auslösung beim Schreiben von Variablen
A0 = 2
Dieses Snippet lässt sich genau so kompilieren, wie wir es ohne die Transformation geschrieben hätten, indem wir automatisch .value
nach Verweisen auf die Variablen anhängen. Mit Reactivity Transform wird das Reaktivitätssystem von Vue zu einem hybriden System.
Debugging der Reaktivität
Es ist großartig, dass Vue's Reaktivitätssystem automatisch Abhängigkeiten verfolgt, aber in einigen Fällen möchten wir vielleicht herausfinden, was genau verfolgt wird oder was eine Komponente zum erneuten Rendern veranlasst.
Debugging-Haken für Komponenten
We can debug what dependencies are used during a component's render and which dependency is triggering an update using the debugger
statement in the callbacks to interactively inspect the dependency:
vue
<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'
onRenderTracked((event) => {
debugger
})
onRenderTriggered((event) => {
debugger
})
</script>
TIP
Komponentendebug-Hooks funktionieren nur im Entwicklungsmodus.
Die Debug-Ereignisobjekte haben den folgenden Typ:
ts
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type:
| TrackOpTypes /* 'get' | 'has' | 'iterate' */
| TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
Computergestütztes Debugging
Wir können berechnete Eigenschaften debuggen, indem wir computed()
ein zweites Optionsobjekt mit onTrack
und onTrigger
Callbacks übergeben:
onTrack
wird aufgerufen, wenn eine reaktive Eigenschaft oder eine Referenz als Abhängigkeit verfolgt wird.onTrigger
wird aufgerufen, wenn der Watcher-Callback durch die Mutation einer Abhängigkeit ausgelöst wird.
Beide Callbacks empfangen Debugger-Ereignisse im gleichen Format wie Komponenten-Debug-Hooks:
js
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// triggered when count.value is tracked as a dependency
debugger
},
onTrigger(e) {
// triggered when count.value is mutated
debugger
}
})
// access plusOne, should trigger onTrack
console.log(plusOne.value)
// mutate count.value, should trigger onTrigger
count.value++
TIP
onTrack
und onTrigger
Die berechneten Optionen funktionieren nur im Entwicklungsmodus.
Watcher-Debugging
Ähnlich wie bei computed()
unterstützen Watcher auch die Optionen onTrack
und onTrigger
:
js
watch(source, callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
watchEffect(callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
TIP
onTrack
und onTrigger
Watcher-Optionen funktionieren nur im Entwicklungsmodus.
Integration mit externen staatlichen Systemen
Das Reaktivitätssystem von Vue funktioniert durch tiefe Konvertierung von einfachen JavaScript-Objekten in reaktive Proxies. Die tiefe Konvertierung kann unnötig oder manchmal unerwünscht sein, wenn man mit externen Zustandsverwaltungssystemen integriert (z.B. wenn eine externe Lösung auch Proxies verwendet).
Die allgemeine Idee der Integration von Vue's Reaktivitätssystem mit einer externen Zustandsverwaltungslösung ist es, den externen Zustand in einer shallowRef
zu halten. Ein shallow ref ist nur reaktiv, wenn auf seine Eigenschaft .value
zugegriffen wird - der innere Wert bleibt intakt. Wenn sich der externe Zustand ändert, ersetzen Sie den ref-Wert, um Aktualisierungen auszulösen.
Unveränderliche Daten
Wenn Sie eine Rückgängig-/Wiederherstellungsfunktion implementieren, möchten Sie wahrscheinlich bei jeder Benutzereingabe einen Schnappschuss des Anwendungsstatus erstellen. Allerdings ist das veränderbare Reaktivitätssystem von Vue dafür nicht am besten geeignet, wenn der Zustandsbaum groß ist, da die Serialisierung des gesamten Zustandsobjekts bei jeder Aktualisierung sowohl in Bezug auf die CPU- als auch auf die Speicherkosten teuer sein kann.
Unveränderliche Datenstrukturen lösen dieses Problem, indem sie die Zustandsobjekte niemals verändern - stattdessen werden neue Objekte erstellt, die die gleichen, unveränderten Teile mit den alten teilen. Es gibt verschiedene Möglichkeiten, unveränderliche Daten in JavaScript zu verwenden, aber wir empfehlen die Verwendung von Immer mit Vue, weil es die Verwendung unveränderlicher Daten unter Beibehaltung der ergonomischeren, veränderbaren Syntax ermöglicht.
Wir können Immer mit Vue über ein einfaches Composable integrieren:
js
import produce from 'immer'
import { shallowRef } from 'vue'
export function useImmer(baseState) {
const state = shallowRef(baseState)
const update = (updater) => {
state.value = produce(state.value, updater)
}
return [state, update]
}
Versuchen Sie es auf dem Spielplatz
Zustandsmaschinen
Der Zustandsautomat ist ein Modell zur Beschreibung aller möglichen Zustände, in denen sich eine Anwendung befinden kann, und aller Möglichkeiten, wie sie von einem Zustand in einen anderen übergehen kann. Während es für einfache Komponenten überflüssig sein mag, kann es helfen, komplexe Zustandsabläufe robuster und handhabbarer zu machen.
Eine der beliebtesten State-Machine-Implementierungen in JavaScript ist XState. Hier ist ein Composable, das sich damit integrieren lässt:
js
import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'
export function useMachine(options) {
const machine = createMachine(options)
const state = shallowRef(machine.initialState)
const service = interpret(machine)
.onTransition((newState) => (state.value = newState))
.start()
const send = (event) => service.send(event)
return [state, send]
}
Versuchen Sie es auf dem Spielplatz
RxJS
RxJS ist eine Bibliothek für die Arbeit mit asynchronen Ereignisströmen. Die VueUse Bibliothek stellt das @vueuse/rxjs
Add-on zur Verfügung, um RxJS Streams mit dem Reaktivitätssystem von Vue zu verbinden.