Към съдържанието
wannacode

Изчислено състояние

Накратко: Изчисленото състояние е стойност, която се смята от състоянието (брой, сума, филтриран списък), вместо да се пази отделно; computed пази резултата до следващата промяна.

Проблем

Сумата изостава

Кошницата има бял хляб и прясно мляко. Натисни „+“ на хляба: броят и сумата растат. Сега махни млякото.

Vue

Редът изчезва, броят пада на 3, а сумата остава 5,20 €. Компонентът пази три състояния: items, count и total. Всеки клик трябва да смени и трите, а функцията за махане сменя две. Това е същото разминаване като в урока „Списък от данни“, само че вътре в компонент. Библиотеката рисува вярно това, което ѝ дадеш. Даваш ѝ две истини за една кошница.

Идеята

Изчисли, не пази

Попитай за всяко състояние: може ли да го изчисля от другото състояние или от props? Ако може, то не е състояние. Броят е сборът от количествата в items. Сумата е сборът от цена по количество. И двете се изчисляват.

Такава стойност се казва изчислено състояние (derived state). Пазиш само items, а останалото смяташ от тях всеки път, когато компонентът се рисува.

const items = ref(START);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));

<script setup> се пуска веднъж, при създаването на компонента. Затова сметката е във функция, дадена на computed. Vue помни, че тя чете items, и я пуска наново, когато items се сменят. В шаблона пишеш {{ total }}, без .value.

Сега функциите more и remove сменят само items. Няма какво да забравят.

Виж го

Виж изчисленото и копието

Вляво е кошницата и копие на сумата, пазено отделно. Вдясно са трите изчислени стойности.

<script setup>
const items = ref(START);
const savedTotal = ref(26.9);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0),
);
const total = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0),
);
const free = computed(() => total.value >= 30);
function more(item) {
item.qty++;
savedTotal.value += item.price;
}
function remove(item) {
items.value = items.value.filter((i) => i.id !== item.id);
}
</script>
Стъпка по стъпка, с думи
  1. Вляво е състоянието: кошницата items. Под нея има второ състояние, savedTotal: същата сума, пазена отделно.
  2. Вдясно са count, total и free: computed стойности. Vue помни, че четат items, и ги смята наново, когато items се сменят: 26,90 €, остават 3,10 € до безплатна доставка.
  3. more(сирене): items се сменя и трите стрелки светват. Сумата е 34,80 €, доставката е безплатна. more() обновява на ръка и savedTotal, затова копието още е вярно.
  4. remove(сирене): items се сменя, а count, total и free следват сами: 3,20 €. Ред за savedTotal в remove() никой не е написал.
  5. Копието казва 34,80 €, а кошницата е 3,20 €. Грешка няма, има невярно число: състоянието и копието му са се разминали.
  6. Правилото: пази само това, което не може да се изчисли. Всичко, което следва от състоянието, се смята от него, и копие за забравяне няма.

Изчислените стойности следват кошницата при всяка промяна, защото нямат собствена памет. Копието следва само докато някой се сеща да го обнови. Първата функция, която забрави, го прави невярно.

Пробвай

Изчисли броя и сумата

Кошницата пази count и total като отделни състояния и remove() забравя сумата. Махни двете състояния и изчисли броя и сумата от items. Проверката натиска „+“ на хляба, после маха млякото и гледа броя и сумата.

Vue

Пробвай

Попълни

Броят се изчислява от кошницата. Какво стои на празното място?

const items = ref(START);
const count = (() => items.value.reduce((sum, i) => sum + i.qty, 0));

Идеята

Кога да пазиш резултата

computed вече пази резултата. Той се смята, когато го прочетеш за първи път. После, докато данните, които е прочел, не се сменят, връща запомнената стойност. Затова два реда в шаблона с {{ total }} смятат сумата веднъж.

const sorted = computed(() => products.value.toSorted(byPrice));

Функция, извикана в шаблона ({{ totalOf(items) }}), също дава вярна сума. Разликата е, че тя се пуска при всяко рисуване на компонента, а computed само при смяна на зависимостите. Функцията в computed само чете и връща. Не сменя състояние и не вика fetch.

Идеята

Филтърът също е изчислен

Каталогът има бутони „Всички“, „Хляб“ и „Мляко“. Изкушението е да пазиш два масива: всички продукти и видимите. Но видимите следват от два други факта: пълния списък и избраната категория. Пазиш само категорията:

const category = ref('all');
const visible = computed(() =>
category.value === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category.value),
);

Същото е с подреждането: пазиш „по цена“ или „по име“, а подреденият списък го изчисляваш. Внимавай с sort(): той подрежда масива на място, тоест сменя състоянието. toSorted() връща нов масив и оставя стария.

Пробвай

Само категорията

Бутоните сменят категорията, но списъкът винаги показва всичко. Изчисли видимите продукти от категорията и покажи „Показани: 3 от 5“ над списъка. Проверката избира „Мляко“, после „Хляб“, после „Всички“.

Vue

Идеята

Копие на prop

Същата грешка има и между компоненти. App дава намаление 20% и подава новите цени на всяка ProductCard. Картата обаче е запомнила цената, която е получила първия път:

Vue

Натисни „Намаление 20%“. Бутонът е натиснат, а хлябът още е 1,20 €.

const price = ref(props.product.price) копира числото веднъж, когато <script setup> се пуска. После props.product е нов, но price е отделна кутия, която никой не сменя.

Поправката е да не копираш: показвай product.price направо. Ако картата трябва да покаже нещо, изчислено от prop (цена с ДДС, цена на брой), изчисли го при рисуване с computed(() => props.product.price * …).

Пробвай

Намери грешката

Купувачът натиска „+“: количеството на реда става 3, а сумата на реда остава като за 2. Кой ред е виновен?

Избери реда с грешката.

По-точно, за любопитните

Кога копието е нарочно. Понякога искаш стойност отвън само за начало: формуляр за редакция започва с текущото име, а после човекът пише. Тогава копието е правилно, но го кажи в името: prop initialName, състояние name. Който чете кода, вижда, че промените отвън не се следят.

computed със запис. computed({ get, set }) дава стойност, която може и да се присвои: set превежда новата стойност към истинското състояние. Ползва се рядко, например за поле, свързано с част от обект.

computed е мързелив. Ако никой не чете total, сметката не се прави. Затова вътре няма странични ефекти: не знаеш кога и колко пъти ще се пусне. За действие при промяна има watch, следващия модул.

Options API. В по-стария запис изчислените стойности са в обекта computed: { total() { … } }. Идеята е същата.

Същото важи за данни от сървъра. Ако сървърът връща продуктите, а ти пазиш отделно „брой продукти“, след ново зареждане двете могат да се разминат. Пази отговора, а броя изчислявай.

Упражнение

Безплатна доставка

Кошницата има бутони „−“ и „+“ за всеки ред (при „−“ на последната бройка редът изчезва). Под списъка покажи „Продукти: 4“, „Общо: 18,20 €“ и „До безплатна доставка: още 11,80 €“, а над 30 € (FREE_FROM) само „Доставката е безплатна“. Всичко се изчислява от items. Проверката добавя сирене до 34,00 €, после маха до под 30 € и маха хляба.

Vue

Тест

Провери се

1. Кошницата пази items. Кое от тези трябва да е отделно състояние?
2. Каква е разликата между computed и функция, извикана в шаблона?
3. const price = ref(props.product.price); в ProductCard. Родителят подава нова цена. Какво показва картата?
4. Списъкът се филтрира по категория. Какво да е в състоянието?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·