Обновяване на обекти и масиви
Накратко: Обект или масив в състоянието на Vue се мени на място, защото Vue следи промените през прокси; работи и нов масив в .value, но не и обикновен масив без ref.
Проблем
„Махни“ не маха
Кошницата има три реда, всеки с бутон „Махни“. Натисни „Махни“ на хляба.
Конзола
Конзолата казва редове: 2. На екрана са трите реда и старата сума. Данните са сменени, а екранът не ги следва, и грешка няма.
remove слага нов масив без хляба в cart. Новата стойност е в променливата. Защо Vue не рисува?Идеята
Как библиотеката вижда промяната
С число е ясно: 1 и 2 са различни. Масивът и обектът обаче са референции: променливата сочи обект в паметта. Има два въпроса. Дали променливата сочи друг обект? И дали съдържанието на същия обект е сменено? Всяка библиотека пита по свой начин, и тук разликата се вижда в кода.
Vue пита втория въпрос. ref([...]) обвива масива в прокси (Proxy): обект, който стои пред масива и вижда всяко четене и писане. push, splice и line.qty++ минават през проксито. То записва промяната и казва на шаблона, който чете масива, да тръгне наново. Затова във Vue масивът се сменя на място, и това е нормалният път.
Проксито обаче не вижда променливата, която го сочи. В примера let cart = reactive([...]) и после cart = cart.filter(...): променливата вече сочи нов, обикновен масив. Шаблонът чете стария масив през старото прокси и никой не му казва. С const cart = ref([...]) новият масив влиза в cart.value, а ref-ът вижда тази смяна.
Виж го
Виж: същият масив или нов
<script setup>const cart = ref([bread]);
function addMilk() { cart.value.push(milk);}
function addCheese() { cart.value = [...cart.value, cheese];}</script>Стъпка по стъпка, с думи
- cart е ref. В него е масивът, обвит в прокси: обект, който стои пред масива и вижда всяко четене и писане.
- cart.value.push(milk) минава през проксито. То записва: масивът се смени.
- Проксито казва на шаблона, който чете cart, че трябва да тръгне наново.
- Шаблонът тръгва и екранът показва двата реда.
- Може и така: нов масив в cart.value. Ref-ът вижда смяната на .value.
- Ref-ът казва на шаблона и той тръгва наново с новия масив.
- Екранът показва трите реда. Във Vue и двата пътя работят.
- Правилото във Vue: сменяй през ref-а. Обикновена променлива или копие на число Vue не вижда.
push през проксито на масива, а новият масив през .value. Vue не вижда само това, което заобикаля и двете.Идеята
Промяна през проксито
Кошницата има три действия: добави ред, смени броя на един ред, махни ред.
// добавиcart.value.push({ ...product, qty: 1 });
// смени един ред: редът също е проксиconst line = cart.value.find((l) => l.id === id);line.qty++;
// махниconst index = cart.value.findIndex((l) => l.id === id);cart.value.splice(index, 1);- Vue следи и обектите вътре в масива:
lineотcart.valueе прокси иline.qty++се вижда. - Работи и другият път: нов масив в
cart.value, напримерcart.value = cart.value.filter((l) => l.id !== id). Ref-ът вижда смяната на.value. - Не се вижда промяна, която заобикаля ref-а: обикновена променлива с нов масив или копие на число.
Пробвай
Поправи „Махни“
Това е кошницата от началото. Поправи remove така, че редът да изчезва и сумата да се смята наново. Проверката маха хляба, после сиренето.
Конзола
Решение
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const cart = ref([
{ id: 1, name: 'Бял хляб', price: 1.2, qty: 2 },
{ id: 5, name: 'Прясно мляко', price: 1.6, qty: 1 },
{ id: 7, name: 'Сирене', 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>Кошницата</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="`Махни ${line.name}`">
Махни
</button>
</li>
</ul>
<p>Общо: {{ 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(...) става const cart = ref(...) с import { ref } from 'vue', а remove е cart.value = cart.value.filter((line) => line.id !== id). Може и splice през cart.value. Сумата чете cart.value, затова и тя тръгва наново. Обикновен масив без ref не работи: splice го сменя, но Vue не го следи.Пробвай
Намери грешката
„+“ не вдига броя. Кой ред е виновен?
Избери реда с грешката.
Ред 2: let { qty } = line копира числото в нова променлива. qty++ сменя копието, а редът в кошницата остава същият. Трябва line.qty++.
Идеята
Един ред от масива
Купувачът натиска „+“ на хляба. Трябва да се смени qty на един обект, а той е в масив.
Във Vue line.qty++ е правилният път: редът от cart.value е прокси и Vue вижда промяната. Грешката е да извадиш числото от обекта:
let { qty } = line; // копие на числотоqty++; // сменя копиетоДеструктурирането и let n = line.qty дават копие. Vue следи обекта line, а не всяка променлива, в която е било числото. Пиши в обекта: line.qty++.
По-точно, за любопитните
Дълбоко следене. ref и reactive правят следими и обектите вътре, при първото им четене. shallowRef следи само .value: с него промяна вътре не се вижда и се слага нов масив, както в React.
reactive и деструктуриране. const { total } = reactive({ total: 0 }) дава число, а не следима стойност. toRefs(obj) превръща полетата в ref-ове, които остават свързани с обекта.
Новият масив. cart.value = [...cart.value, line] работи и прави нов масив, както се прави в React. Полезно е, когато старият масив трябва да остане същият, например за „Отмени“.
structuredClone не приема прокси: structuredClone(cart.value) хвърля грешка. Първо се взима обикновеният масив с toRaw(cart.value).
Пробвай
„+“ и „−“
Направи more и less. „+“ вдига броя на реда с 1, а „−“ го сваля с 1. Когато броят е 1, „−“ маха реда. Сумата следва броя.
Конзола
Решение
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const cart = ref([
{ id: 1, name: 'Бял хляб', price: 1.2, qty: 2 },
{ id: 5, name: 'Прясно мляко', price: 1.6, qty: 1 },
{ id: 7, name: 'Сирене', 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>Кошницата</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="`По-малко ${line.name}`">−</button>
<button type="button" @click="more(line.id)" :aria-label="`Още ${line.name}`">+</button>
<button type="button" @click="remove(line.id)" :aria-label="`Махни ${line.name}`">
Махни
</button>
</li>
</ul>
<p>Общо: {{ 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 намира реда в cart.value и прави line.qty++. less при qty === 1 вика remove, а иначе прави line.qty--. С let { qty } = line и qty++ редът остава същият: сменя се копие. Без проверката за 1 редът стига до „0 ד и остава в кошницата.
Упражнение
Каталог и кошница
Каталогът има четири продукта с бутон „Добави“ (името за екранния четец: „Добави Бял хляб“). Направи add и remove. Първият клик на „Добави“ прави ред с брой 1, а следващите вдигат броя на същия ред, вместо да правят нов. „Махни“ маха реда. Когато кошницата е празна, пише „Кошницата е празна“.
Конзола
Решение
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const products = [
{ id: 1, name: 'Бял хляб', price: 1.2 },
{ id: 5, name: 'Прясно мляко', price: 1.6 },
{ id: 7, name: 'Сирене', price: 7.9 },
{ id: 10, name: 'Ябълки', 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>Магазинът</h1>
<ul>
<li v-for="p in products" :key="p.id">
{{ p.name }}, {{ money(p.price) }}
<button type="button" @click="add(p)" :aria-label="`Добави ${p.name}`">
Добави
</button>
</li>
</ul>
<h2>Кошницата</h2>
<p v-if="cart.length === 0">Кошницата е празна</p>
<ul aria-label="Кошницата">
<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="`Махни ${line.name}`">
Махни
</button>
</li>
</ul>
<p>Общо: {{ 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 търси реда с find. Ако го има: found.qty++. Ако го няма: cart.value.push({ ...product, qty: 1 }). { ...product, qty: 1 } е нов обект и продуктът в каталога не се пипа. remove е splice или filter през cart.value. С const cart = [] без ref кошницата остава празна на екрана, а с push при всеки клик се появява нов ред за същия продукт.Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Първият компонент · Списъци и ключове · Условия: празно, грешка, зарежда · Състояние
- Продължава в: Изчислено състояние · Вдигане на състоянието · Общо състояние · Витрината на магазина
- Задачи по темата: Състояние