Състояние
Накратко: Състоянието (state) е стойност, която компонентът помни между рисуванията; във Vue се пази с ref, а всяка промяна обновява компонента в страницата.
Проблем
Бутонът не брои
Картата на продукта има бутон „Добави“ и брояч „В кошницата“. Натисни „Добави“ три пъти.
Конзола
Конзолата казва count = 1, 2, 3. На картата пише 0. Грешка няма. Променливата count се сменя, а екранът не я следва. Къде е разликата?
Идеята
Обикновената променлива не стига
Картата не се рисува от твоя код, а от библиотеката. Тя рисува наново само когато знае, че нещо се е сменило. count++ сменя променливата, но не казва на никого.
<script setup> тече веднъж за всяка карта, а шаблона го пуска Vue. Vue пуска шаблона наново само когато се смени нещо, което следи. Обикновена let променлива Vue не следи. Кликът сменя count, но шаблонът не тръгва наново, затова картата остава с 0.
Трябват данни, които компонентът помни между две рисувания и чиято промяна го рисува наново. Това се казва състояние (state).
Идеята
useState и ref
<script setup>import { ref } from 'vue';
const count = ref(0);
function add() { count.value++;}</script>
<template> <p>В кошницата: {{ count }}</p></template>ref(0)връща кутия, а числото е вcount.value. В скрипта пишеш.value.- В шаблона Vue разопакова кутията: пишеш
{{ count }}, без.value. - Когато шаблонът чете
count, Vue го запомня. Когатоcount.valueсе смени, Vue пуска шаблона наново.
Обработчикът на клика се дава като функция: @click="add". Във Vue може и @click="add()" или @click="count++": шаблонът сам ги обвива във функция, която тече при клик.
Конзола
Виж го
Виж какво става при клик
<script setup>const count = ref(0);
function addThree() { count.value++; count.value++; count.value++;}</script>
<template> <article> <button @click="addThree">+3</button> <p>В кошницата: {{ count }}</p> </article></template>Стъпка по стъпка, с думи
- Картата пази count в ref: кутия, чиято стойност е count.value. Vue следи кой я чете.
- Vue пуска шаблона. Той чете count и връща описание: бутон и текст с 0. Vue запомня, че шаблонът чете count.
- Първия път описанието става истински елементи в DOM-а. Страницата вече показва „В кошницата: 0“.
- Клик на +3: браузърът вика addThree.
- Три пъти count.value++. Кутията се сменя веднага: 1, 2, 3. Vue планира шаблона да тръгне наново, но само веднъж.
- Обработчикът свърши. Vue пуска шаблона веднъж, с count = 3, и той връща ново описание.
- Vue сравнява новото описание със старото. Бутонът е същият, сменен е само текстът. В DOM-а се сменя само той.
- Броячът показва 3. Промяната на ref е веднага, а рисуването е веднъж, след обработчика.
Картата не се строи наново. Библиотеката сравнява новото описание със старото и сменя в DOM-а само разликата: текста. Бутонът е същият елемент и фокусът остава на него. Това е разликата с render() от модула за DOM, която сменяше всичко.
Пробвай
Поправи брояча
Това е картата от началото. Направи count състояние, така че след всеки клик на „Добави“ картата да показва новия брой.
Конзола
Решение
ProductCard.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(0);
function add() {
count.value++;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<button type="button" @click="add" :aria-label="`Добави ${product.name}`">
Добави
</button>
<p>В кошницата: {{ count }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const product = { id: 1, name: 'Бял хляб', price: 1.2 };
</script>
<template>
<main>
<h1>Магазинът</h1>
<ProductCard :product="product" />
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
let count = 0 става const count = ref(0), с import { ref } from 'vue', а в add пишеш count.value++. В шаблона остава {{ count }}: с {{ count.value }} текстът е празен, защото там count вече е числото.Идеята
Кутията и числото
Минералната вода се продава и на стек от 6 бутилки. Удобно е бутонът за стек да вика add() шест пъти. Тук двете библиотеки се държат различно.
count.value винаги е текущото число, затова count.value++ шест пъти дава 6. Шаблонът при това не тръгва шест пъти. Vue събира промените и го пуска веднъж, след обработчика.
Грешката тук е друга. Кутията е count, а count.value е число. Числото, сложено в друга променлива, е копие:
let bottles = count.value;bottles += 6; // сменя копието, count остава същиятVue следи кутията, а не всяка променлива, в която е било числото. Пиши в кутията: count.value += 6.
Пробвай
Намери грешката
„Добави стек“ трябва да добави 6 бутилки. Кой ред е виновен?
Избери реда с грешката.
Ред 5: сменя се bottles, копие на числото. Кутията count не се пипа и шаблонът не тръгва. Трябва count.value += 6.
Пробвай
Стек вода
Направи addBox() така, че „Добави стек (6 бр.)“ да добавя 6 бутилки към това, което вече е в кошницата. Проверката натиска „Добави“ веднъж, после стека два пъти и очаква 1, 7 и 13.
Конзола
Решение
ProductCard.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(0);
function add() {
count.value++;
}
function addBox() {
count.value += 6;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<button type="button" @click="add" :aria-label="`Добави ${product.name}`">
Добави
</button>
<button type="button" @click="addBox">Добави стек (6 бр.)</button>
<p>В кошницата: {{ count }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const product = { id: 15, name: 'Минерална вода', price: 0.9 };
</script>
<template>
<main>
<h1>Магазинът</h1>
<ProductCard :product="product" />
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
count.value += 6 сменя кутията и шаблонът тръгва наново. let n = count.value; n += 6 сменя копие и картата не мърда. count.value = 6 не добавя, а слага 6, и след „Добави“ броячът е 6, а не 7.Идеята
Всяка карта помни своето
Състоянието е на екземпляра, а не на кода. Каталогът има две карти от един и същ ProductCard, и всяка има свой count:
Конзола
Натисни „Добави“ на хляба два пъти и на млякото веднъж: 2 и 1. Библиотеката пази състоянието на всяка карта до мястото ѝ в дървото на компонентите. Махнеш ли картата от страницата, състоянието ѝ изчезва с нея. Когато две карти трябва да показват едно и също число, то трябва да е в общия им родител. За това е урокът за споделеното състояние в този модул.
По-точно, за любопитните
ref и reactive. reactive({ count: 0 }) прави обекта следим и няма .value, но работи само с обекти и не може да се смени целият. ref работи с всяка стойност, затова документацията на Vue го препоръчва като основен.
Кога тръгва шаблонът. Vue пуска шаблона наново в следващата микрозадача, след като кодът в момента свърши. await nextTick() чака това рисуване, например за да прочетеш новия DOM.
Разопаковането. В шаблона се разопаковат ref-ове от най-горно ниво на <script setup>. Ref вътре в обикновен обект ({ total: ref(0) }) не се разопакова в израз като {{ cart.total + 1 }}.
Options API. По-стар Vue код пише компонента като обект с data(), methods и this.count. Новият код е със <script setup> и Composition API.
Упражнение
Избери броя
Каталогът има три карти. Във всяка добави брой, който започва от 1, бутон „−“ (име за екранния четец: „По-малко Бял хляб“) и бутон „+“ („Повече Бял хляб“) и ред „2 × 1,20 € = 2,40 €“ с броя, цената и сумата. „−“ е забранен (disabled), когато броят е 1. Всяка карта брои сама.
Конзола
Решение
ProductCard.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(1);
function less() {
count.value--;
}
function more() {
count.value++;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<p>
<button type="button" @click="less" :disabled="count === 1" :aria-label="`По-малко ${product.name}`">−</button>
<button type="button" @click="more" :aria-label="`Повече ${product.name}`">+</button>
</p>
<p>{{ count }} × {{ money(product.price) }} = {{ money(count * product.price) }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const products = [
{ id: 1, name: 'Бял хляб', price: 1.2 },
{ id: 5, name: 'Прясно мляко', price: 1.6 },
{ id: 7, name: 'Сирене', price: 7.9 },
];
</script>
<template>
<main>
<h1>Магазинът</h1>
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
ProductCard.vue: const count = ref(1), less прави count.value--, а more прави count.value++. „−“ е :disabled="count === 1": така броят не може да падне под 1. Сумата е money(count * product.price) направо в шаблона: тя следва от count и не е отделно състояние. За това е урокът за изчисленото състояние. Ако ref е в отделен обикновен <script>, той е един за всички карти и „+“ на хляба сменя и сиренето.Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Събития · Първият компонент · Props: данни отгоре · Списъци и ключове
- Продължава в: Обновяване на обекти и масиви · Изчислено състояние · Вдигане на състоянието · Ефекти
- Задачи по темата: Състояние