Objekte und Arrays aktualisieren
Kurz gesagt: Ein Objekt oder Array im Vue-Zustand wird an Ort und Stelle geändert, weil Vue Änderungen über einen Proxy beobachtet; ein neues Array in .value funktioniert auch, ein einfaches Array ohne ref aber nicht.
Problem
„Entfernen“ entfernt nichts
Der Warenkorb hat drei Zeilen, jede mit einem Button „Entfernen“. Klick bei dem Brot auf „Entfernen“.
Konsole
Die Konsole sagt Zeilen: 2. Der Bildschirm zeigt alle drei Zeilen und die alte Summe. Die Daten haben sich geändert, der Bildschirm folgt ihnen nicht, und es gibt keinen Fehler.
remove legt ein neues Array ohne das Brot in cart. Der neue Wert steht in der Variablen. Warum zeichnet Vue nicht?Die Idee
Wie die Bibliothek eine Änderung sieht
Bei einer Zahl ist es klar: 1 und 2 sind verschieden. Ein Array und ein Objekt sind dagegen Referenzen: Die Variable zeigt auf ein Objekt im Speicher. Es gibt zwei Fragen. Zeigt die Variable auf ein anderes Objekt? Und hat sich der Inhalt desselben Objekts geändert? Jede Bibliothek fragt auf ihre Art, und hier zeigt sich der Unterschied im Code.
Vue stellt die zweite Frage. ref([...]) packt das Array in einen Proxy: ein Objekt, das vor dem Array steht und jedes Lesen und Schreiben sieht. push, splice und line.qty++ gehen durch den Proxy. Er hält die Änderung fest und sagt dem Template, das das Array liest, dass es erneut laufen soll. Deshalb ändert man in Vue das Array an Ort und Stelle, und das ist der normale Weg.
Der Proxy sieht aber die Variable nicht, die auf ihn zeigt. Im Beispiel let cart = reactive([...]) und dann cart = cart.filter(...): Die Variable zeigt jetzt auf ein neues, einfaches Array. Das Template liest das alte Array über den alten Proxy, und niemand sagt es ihm. Bei const cart = ref([...]) kommt das neue Array nach cart.value, und die Ref sieht diese Änderung.
Sieh es dir an
Sieh: dasselbe Array oder ein neues
<script setup>const cart = ref([bread]);
function addMilk() { cart.value.push(milk);}
function addCheese() { cart.value = [...cart.value, cheese];}</script>Schritt für Schritt, in Worten
- cart ist eine Ref. Darin steckt das Array, in einen Proxy gepackt: ein Objekt, das vor dem Array steht und jedes Lesen und Schreiben sieht.
- cart.value.push(milk) geht durch den Proxy. Der Proxy hält fest: Das Array hat sich geändert.
- Der Proxy sagt dem Template, das cart liest, dass es erneut laufen muss.
- Das Template läuft, und der Bildschirm zeigt beide Zeilen.
- Auch das geht: ein neues Array in cart.value. Die Ref bemerkt die Änderung von .value.
- Die Ref sagt es dem Template, und das Template läuft mit dem neuen Array erneut.
- Der Bildschirm zeigt drei Zeilen. In Vue funktionieren beide Wege.
- Die Regel in Vue: Ändere über die Ref. Eine einfache Variable oder die Kopie einer Zahl sieht Vue nicht.
push durch den Proxy des Arrays und das neue Array durch .value. Vue übersieht nur das, was an beiden vorbeigeht.Die Idee
Eine Änderung durch den Proxy
Der Warenkorb hat drei Aktionen: eine Zeile hinzufügen, die Menge einer Zeile ändern, eine Zeile entfernen.
// hinzufügencart.value.push({ ...product, qty: 1 });
// eine Zeile ändern: Auch die Zeile ist ein Proxyconst line = cart.value.find((l) => l.id === id);line.qty++;
// entfernenconst index = cart.value.findIndex((l) => l.id === id);cart.value.splice(index, 1);- Vue beobachtet auch die Objekte im Array:
lineauscart.valueist ein Proxy, undline.qty++wird gesehen. - Der andere Weg funktioniert auch: ein neues Array in
cart.value, zum Beispielcart.value = cart.value.filter((l) => l.id !== id). Die Ref sieht die Änderung von.value. - Eine Änderung, die an der Ref vorbeigeht, wird nicht gesehen: eine einfache Variable mit einem neuen Array oder eine Kopie einer Zahl.
Probier es aus
Repariere „Entfernen“
Das ist der Warenkorb vom Anfang. Repariere remove so, dass die Zeile verschwindet und die Summe neu berechnet wird. Die Prüfung entfernt das Brot, dann den Käse.
Konsole
Lösung
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const cart = ref([
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2 },
{ id: 5, name: 'Frischmilch', price: 1.6, qty: 1 },
{ id: 7, name: 'Weißkäse', price: 7.9, qty: 1 },
]);
function remove(id) {
cart.value = cart.value.filter((line) => line.id !== id);
}
const total = () => cart.value.reduce((sum, line) => sum + line.price * line.qty, 0);
</script>
<template>
<main>
<h2>Dein Warenkorb</h2>
<ul>
<li v-for="line in cart" :key="line.id">
{{ line.name }}: {{ line.qty }} × {{ money(line.price) }}
<button type="button" @click="remove(line.id)" :aria-label="`Entfernen ${line.name}`">
Entfernen
</button>
</li>
</ul>
<p>Gesamt: {{ money(total()) }}</p>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
let cart = reactive(...) wird const cart = ref(...) mit import { ref } from 'vue', und remove ist cart.value = cart.value.filter((line) => line.id !== id). splice über cart.value funktioniert auch. Die Summe liest cart.value, also läuft auch sie erneut. Ein einfaches Array ohne ref funktioniert nicht: splice ändert es, aber Vue beobachtet es nicht.Probier es aus
Finde den Fehler
„+“ erhöht die Menge nicht. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
Zeile 2: let { qty } = line kopiert die Zahl in eine neue Variable. qty++ ändert die Kopie, und die Zeile im Warenkorb bleibt gleich. Es müsste line.qty++ heißen.
Die Idee
Eine Zeile des Arrays
Die Kundin drückt bei dem Brot auf „+“. Das qty eines Objekts muss sich ändern, und das Objekt steckt in einem Array.
In Vue ist line.qty++ der richtige Weg: Die Zeile aus cart.value ist ein Proxy, und Vue sieht die Änderung. Der Fehler ist, die Zahl aus dem Objekt herauszunehmen:
let { qty } = line; // eine Kopie der Zahlqty++; // ändert die KopieDestrukturieren und let n = line.qty geben eine Kopie. Vue beobachtet das Objekt line, nicht jede Variable, in der die Zahl schon war. Schreib ins Objekt: line.qty++.
Genauer, für Neugierige
Tiefes Beobachten. ref und reactive machen auch die Objekte darin beobachtbar, sobald sie zum ersten Mal gelesen werden. shallowRef beobachtet nur .value: Mit ihr wird eine Änderung im Inneren nicht gesehen, und du setzt ein neues Array ein, wie in React.
reactive und Destrukturieren. const { total } = reactive({ total: 0 }) gibt eine Zahl, keinen beobachteten Wert. toRefs(obj) macht aus den Feldern Refs, die mit dem Objekt verbunden bleiben.
Das neue Array. cart.value = [...cart.value, line] funktioniert und macht ein neues Array, wie du es in React tun würdest. Das ist nützlich, wenn das alte Array gleich bleiben muss, zum Beispiel für „Rückgängig“.
structuredClone nimmt keinen Proxy: structuredClone(cart.value) wirft einen Fehler. Hol zuerst das einfache Array mit toRaw(cart.value).
Probier es aus
„+“ und „−“
Schreibe more und less. „+“ erhöht die Menge der Zeile um 1, und „−“ verringert sie um 1. Ist die Menge 1, entfernt „−“ die Zeile. Die Summe folgt der Menge.
Konsole
Lösung
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const cart = ref([
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2 },
{ id: 5, name: 'Frischmilch', price: 1.6, qty: 1 },
{ id: 7, name: 'Weißkäse', price: 7.9, qty: 1 },
]);
function more(id) {
const line = cart.value.find((l) => l.id === id);
line.qty++;
}
function less(id) {
const line = cart.value.find((l) => l.id === id);
if (line.qty === 1) {
remove(id);
} else {
line.qty--;
}
}
function remove(id) {
const index = cart.value.findIndex((line) => line.id === id);
cart.value.splice(index, 1);
}
const total = () => cart.value.reduce((sum, line) => sum + line.price * line.qty, 0);
</script>
<template>
<main>
<h2>Dein Warenkorb</h2>
<ul>
<li v-for="line in cart" :key="line.id">
{{ line.name }}: {{ line.qty }} × {{ money(line.price) }}
<button type="button" @click="less(line.id)" :aria-label="`Weniger ${line.name}`">−</button>
<button type="button" @click="more(line.id)" :aria-label="`Mehr ${line.name}`">+</button>
<button type="button" @click="remove(line.id)" :aria-label="`Entfernen ${line.name}`">
Entfernen
</button>
</li>
</ul>
<p>Gesamt: {{ money(total()) }}</p>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
more findet die Zeile in cart.value und macht line.qty++. less ruft bei qty === 1 remove auf, sonst macht es line.qty--. Mit let { qty } = line und qty++ bleibt die Zeile gleich: Eine Kopie ändert sich. Ohne die Prüfung auf 1 erreicht die Zeile „0 ד und bleibt im Warenkorb.
Übung
Katalog und Warenkorb
Der Katalog hat vier Produkte mit einem Button „Hinzufügen“ (der Name für den Screenreader: „Hinzufügen Weißbrot“). Schreibe add und remove. Der erste Klick auf „Hinzufügen“ macht eine Zeile mit der Menge 1, und die nächsten erhöhen die Menge derselben Zeile, statt eine neue zu machen. „Entfernen“ entfernt die Zeile. Ist der Warenkorb leer, steht dort „Der Warenkorb ist leer“.
Konsole
Lösung
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const products = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 10, name: 'Äpfel', price: 2.1 },
];
const cart = ref([]);
function add(product) {
const found = cart.value.find((line) => line.id === product.id);
if (found) {
found.qty++;
} else {
cart.value.push({ ...product, qty: 1 });
}
}
function remove(id) {
const index = cart.value.findIndex((line) => line.id === id);
cart.value.splice(index, 1);
}
const total = () => cart.value.reduce((sum, line) => sum + line.price * line.qty, 0);
</script>
<template>
<main>
<h1>Der Shop</h1>
<ul>
<li v-for="p in products" :key="p.id">
{{ p.name }}, {{ money(p.price) }}
<button type="button" @click="add(p)" :aria-label="`Hinzufügen ${p.name}`">
Hinzufügen
</button>
</li>
</ul>
<h2>Dein Warenkorb</h2>
<p v-if="cart.length === 0">Der Warenkorb ist leer</p>
<ul aria-label="Dein Warenkorb">
<li v-for="line in cart" :key="line.id">
{{ line.name }}: {{ line.qty }} × {{ money(line.price) }}
<button type="button" @click="remove(line.id)" :aria-label="`Entfernen ${line.name}`">
Entfernen
</button>
</li>
</ul>
<p>Gesamt: {{ money(total()) }}</p>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
add sucht die Zeile mit find. Ist sie da: found.qty++. Wenn nicht: cart.value.push({ ...product, qty: 1 }). { ...product, qty: 1 } ist ein neues Objekt, und das Produkt im Katalog bleibt unberührt. remove ist splice oder filter über cart.value. Mit const cart = [] ohne ref bleibt der Warenkorb auf dem Bildschirm leer, und mit nur push fügt jeder Klick eine neue Zeile für dasselbe Produkt hinzu.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Die erste Komponente · Listen und Keys · Bedingungen: leer, Fehler, lädt · Zustand
- Geht weiter in: Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Gemeinsamer Zustand · Das Schaufenster des Shops
- Aufgaben zum Thema: Zustand