Proxy und Getter
Dieses Kapitel ist ein Exkurs. Für die Prüfung brauchst du es nicht, und Vue funktioniert auch, ohne dass du es gelesen hast. Es zeigt aber, wie Vue merkt, dass sich Daten geändert haben, und warum manche Fehler mit Vue so aussehen, wie sie aussehen. In Teil 5 kommt der Kurs darauf zurück.
Lernziele
Nach diesem Kapitel kannst du
- Getter und Setter schreiben und erklären, wann ihr Code läuft,
- mit einem
Proxydas Lesen und Schreiben eines Objekts abfangen, - erklären, wie ein reaktives System mit
trackundtriggerÄnderungen bemerkt, - beschreiben, worin sich das Beispiel von Vue unterscheidet.
Getter und Setter
Abschnitt betitelt „Getter und Setter“Ein Getter sieht von aussen aus wie ein Feld, ist aber eine Funktion, die bei jedem Lesen läuft. Ein Setter läuft bei jedem Schreiben. Beide kennst du von den Klassen aus der Einheit Klassen und this, in Objekten funktionieren sie genauso.
const reservation = { from: new Date('2026-11-02'), to: new Date('2026-11-04'), get days() { return (this.to - this.from) / 86_400_000 + 1 },}console.log(reservation.days)reservation.to = new Date('2026-11-06')console.log(reservation.days)
const item = { storedDeposit: 20, get deposit() { return this.storedDeposit }, set deposit(value) { if (value < 10 || value > 80) throw new RangeError('Das Pfand liegt zwischen 10 und 80 Franken') this.storedDeposit = value },}item.deposit = 30console.log(item.deposit)try { item.deposit = 120} catch (error) { console.log(error.message)}days rechnet bei jedem Lesen neu, darum stimmt der Wert auch, nachdem sich das Ende der Reservation geändert hat. Der Setter von deposit prüft jeden neuen Wert, bevor er ihn speichert. Getter und Setter zeigen das Prinzip, auf dem Reaktivität beruht. Lesen und Schreiben eines Felds kann Code auslösen. Sie haben aber eine Grenze. Getter und Setter gelten nur für Felder, die schon beim Schreiben des Codes feststehen.
Ein Proxy legt sich um ein ganzes Objekt. Jeder Zugriff geht zuerst an Funktionen, die du festlegst, die sogenannten Traps. get läuft beim Lesen eines Felds, set beim Schreiben, auch bei Feldern, die es noch gar nicht gibt.
const item = { name: 'Zelt', availableFrom: null }
const watched = new Proxy(item, { get(target, key) { console.log(`gelesen ${String(key)}`) return target[key] }, set(target, key, value) { console.log(`geschrieben ${String(key)} = ${value}`) target[key] = value return true },})
watched.availableFrom = '2026-11-14'console.log(watched.name)console.log(item.availableFrom)Der Proxy meldet jeden Zugriff und gibt ihn danach an das eigentliche Objekt weiter. Darum steht das neue Datum am Schluss auch in item. Ein set muss true zurückgeben, sonst gilt das Schreiben als gescheitert, und in einem Modul meldet JavaScript dann einen TypeError.
Ein kleines reaktives System
Abschnitt betitelt „Ein kleines reaktives System“Mit einem Proxy lässt sich erkennen, welcher Code welche Felder liest. Ändert sich eines dieser Felder später, kann man genau diesen Code wieder ausführen. Das ist der Kern der Reaktivität von Vue.
let activeEffect = nullconst effectsByKey = new Map()
function reactive(object) { return new Proxy(object, { get(target, key) { // Notes which effect reads which key if (activeEffect) { if (!effectsByKey.has(key)) effectsByKey.set(key, new Set()) effectsByKey.get(key).add(activeEffect) } return target[key] }, set(target, key, value) { target[key] = value // Runs every effect that read this key again for (const effect of effectsByKey.get(key) ?? []) effect() return true }, })}
function watchEffect(effect) { activeEffect = effect effect() activeEffect = null}
const counts = reactive({ available: 49, total: 60 })watchEffect(() => console.log(`${counts.available} von ${counts.total} Dingen sind da`))counts.available = 48counts.total = 62watchEffect merkt sich die Funktion als aktiven Effekt und führt sie einmal aus. Während sie läuft, liest sie counts.available und counts.total, und die Trap get notiert für beide Felder, dass dieser Effekt sie braucht. Diesen Schritt nennt Vue track. Schreibt später jemand eines der Felder, führt die Trap set alle notierten Effekte wieder aus. Diesen Schritt nennt Vue trigger. So erscheint nach jeder Änderung eine neue Zeile, ohne dass jemand die Ausgabe von Hand aufruft.
Der Unterschied zu Vue
Abschnitt betitelt „Der Unterschied zu Vue“Das Beispiel zeigt die Idee, Vue setzt sie gründlicher um.
- Vue notiert die Effekte pro Objekt und pro Feld, in einer
WeakMapmit den Objekten als Schlüsseln. Das Beispiel notiert nur pro Feld, darum stören sich zwei Objekte mit gleichen Feldern, wie die Knobelei zeigt. reactive()liefert einen Proxy wie im Beispiel. Für einzelne Werte wie eine Zahl gibt esref(). Weil sich um eine Zahl kein Proxy legen lässt, packt Vue sie in ein Objekt mit einem Getter und Setter fürvalue.- Vue führt Effekte nicht sofort nach jeder Änderung aus, sondern sammelt sie und aktualisiert die Oberfläche einmal, bevor der Browser die Seite neu zeichnet.
- Verschachtelte Objekte macht Vue erst reaktiv, wenn jemand sie liest.
Daraus erklärt sich ein Fehler, den Teil 5 genauer zeigt. Zerlegst du ein reaktives Objekt mit Destrukturieren in einzelne Variablen, liest du die Werte einmal aus dem Proxy. Die Variablen sind danach gewöhnliche Werte, und kein Effekt wird mehr ausgelöst, wenn sich das Objekt ändert.
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben dieses Exkurses laufen direkt im Browser. Ihre Lösungen kannst du mit «Ausführen» starten und mit «Bearbeiten» verändern.
Aufgabe Einstieg
Ein Warenkorb hat eine Liste items mit Dingen und ihrem Pfand. Gib ihm einen Getter total, der die Summe der Pfänder liefert. Prüfe, dass total stimmt, nachdem ein weiteres Ding dazugekommen ist.
Lösung
const basket = { items: [ { name: 'Zelt', deposit: 50 }, { name: 'Schlafsack', deposit: 20 }, ], get total() { return this.items.reduce((sum, item) => sum + item.deposit, 0) },}
console.log(basket.total)basket.items.push({ name: 'Isomatte', deposit: 10 })console.log(basket.total)Der Getter rechnet bei jedem Lesen mit reduce neu, darum stimmt die Summe nach push ohne weiteres Zutun. Ein Feld total, das du beim Anlegen einmal berechnest, wäre danach veraltet.
Aufgabe Vertiefung
Schreib eine Funktion checked(item), die einen Proxy um ein Ding legt. Der Proxy lässt nur Pfänder zwischen 10 und 80 Franken zu und meldet sonst einen RangeError. Alle anderen Felder lassen sich frei ändern.
Lösung
function checked(item) { return new Proxy(item, { set(target, key, value) { if (key === 'deposit' && (typeof value !== 'number' || value < 10 || value > 80)) { throw new RangeError(`Ungültiges Pfand ${value}`) } target[key] = value return true }, })}
const drill = checked({ name: 'Bohrmaschine', deposit: 20 })drill.deposit = 30console.log(drill.deposit)try { drill.deposit = 5} catch (error) { console.log(error.name, error.message)}Die Trap set prüft nur das Feld deposit und lässt alle anderen durch. Anders als ein Setter wirkt der Proxy auf das ganze Objekt, die Prüfung gilt also auch, wenn das Feld beim Anlegen noch gar nicht existierte.
Aufgabe Knobelei
Im reaktiven System des Beispiels stören sich zwei Objekte. Ändert sich bei einem Zelt das Feld available, läuft auch der Effekt einer Bohrmaschine, der nur die Bohrmaschine liest. Baue das System so um, dass es die Effekte pro Objekt und pro Feld notiert, wie Vue.
Lösung
let activeEffect = null// Every reactive object gets its own map from keys to effectsconst effectsByTarget = new WeakMap()
function track(target, key) { if (!activeEffect) return if (!effectsByTarget.has(target)) effectsByTarget.set(target, new Map()) const effectsByKey = effectsByTarget.get(target) if (!effectsByKey.has(key)) effectsByKey.set(key, new Set()) effectsByKey.get(key).add(activeEffect)}
function trigger(target, key) { for (const effect of effectsByTarget.get(target)?.get(key) ?? []) effect()}
function reactive(object) { return new Proxy(object, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) return true }, })}
function watchEffect(effect) { activeEffect = effect effect() activeEffect = null}
const drill = reactive({ name: 'Bohrmaschine', available: true })const tent = reactive({ name: 'Zelt', available: true })watchEffect(() => console.log(drill.name, drill.available))tent.available = falsedrill.available = falseEine WeakMap ordnet jedem Objekt eine eigene Map von Feldern zu Effekten zu. Die Änderung am Zelt findet in der Map des Zelts keinen Effekt, die Ausgabe zeigt nur die beiden Zeilen der Bohrmaschine. Eine WeakMap hält ihre Schlüssel nicht fest. Braucht niemand mehr ein Objekt, darf JavaScript es samt seinen Einträgen aus dem Speicher räumen. Vue verwendet dieselbe Struktur.
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Wann läuft der Code eines Getters?
- Was kann ein Proxy, was ein Setter nicht kann?
- Was notiert ein reaktives System beim Lesen, und was tut es beim Schreiben?
- Warum verliert eine Variable aus einem destrukturierten reaktiven Objekt die Reaktivität?
Antworten
- Bei jedem Lesen des Felds.
- Ein Proxy fängt Zugriffe auf alle Felder eines Objekts ab, auch auf Felder, die erst später dazukommen. Ein Setter gilt nur für das eine Feld, für das er geschrieben ist.
- Beim Lesen notiert es, welcher Effekt welches Feld braucht. Beim Schreiben führt es die Effekte aus, die das geänderte Feld brauchen.
- Destrukturieren liest den Wert einmal aus dem Proxy. Die Variable hält danach einen gewöhnlichen Wert, und Zugriffe auf sie gehen nicht mehr durch den Proxy.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.