Ефекти
Накратко: Ефектът е код за страничните действия (таймер, заявка, заглавие на раздела); във Vue onMounted го пуска, когато компонентът е на страницата, watch при смяна на източника, а почистването спира пуснатото.
Проблем
Банерът си отиде, проверките остават
Баницата със сирене е изчерпана. Купувачът натиска „Следи наличността“ и под бутона се появява банер. Докато банерът е отворен, страницата проверява наличността на всяка секунда. Тук проверката само пише ред в конзолата.
Конзола
Натисни „Следи наличността“, изчакай две секунди и натисни „Спри следенето“. Банерът изчезва, а в конзолата пак идват „Проверка №3“, „Проверка №4“… Отвори и затвори банера още веднъж: сега проверките са два реда на секунда. Таймерът е на браузъра, а не на компонента. Никой не го е спрял.
Идеята
Рисуването само изчислява
Компонентът казва какво да има на екрана: от props и състоянието изчислява разметката. Това е рисуването. Библиотеката го повтаря, когато поиска, и сравнява резултата с предишния.
Затова рисуването трябва да е чисто: да не прави нищо друго, освен да изчисли разметката. Таймерът, заявката към сървъра, смяната на document.title, абонаментът за събития на window правят нещо навън. Те са странични ефекти (side effects).
В <script setup> кодът върви веднъж, при създаването на компонента. Затова таймерът в примера тръгва веднъж на отваряне. Но той тръгва, докато компонентът още се създава, и нищо не го спира, когато компонентът си отиде. Шаблонът е рисуването: Vue го рисува наново, щом се сменят данните, които той чете. И той трябва само да описва какво се вижда.
Страничният ефект има нужда от две неща, които рисуването не дава: момент, в който да тръгне, и момент, в който да спре.
Идеята
Ефект и почистване
Ефектът е код, който компонентът пуска, след като е на страницата. Почистването е кодът, който спира пуснатото, когато компонентът си отиде.
<script setup>import { onMounted, onUnmounted } from 'vue';
const props = defineProps(['product']);
let id;onMounted(() => { id = setInterval(check, 1000);});onUnmounted(() => clearInterval(id));</script>onMounted(функция)казва на Vue: „пусни я, когато компонентът е на страницата“.onUnmounted(функция)се вика, когато компонентът излезе от страницата. Това е почистването.idе извън двете функции, защото и двете трябва да го виждат.
Почистването е огледало на ефекта: setInterval срещу clearInterval, addEventListener срещу removeEventListener, заявка срещу отказ на заявката.
Пробвай
Спри проверките
Таймерът в StockWatcher тръгва направо в компонента и никой не го спира. Премести го в ефект, който тръгва, когато банерът се появи, и го спри, когато банерът си отиде. Проверката отваря банера, чака „Проверка №1“, затваря го и гледа, че „Проверка №2“ не идва.
Конзола
Решение
App.vue<script setup>
import { ref } from 'vue';
import StockWatcher from './StockWatcher.vue';
const PRODUCT = { id: 4, name: 'Баница със сирене' };
const watching = ref(false);
</script>
<template>
<main>
<h1>{{ PRODUCT.name }}</h1>
<p>Изчерпан. Ще се появи отново скоро.</p>
<button type="button" @click="watching = !watching">
{{ watching ? 'Спри следенето' : 'Следи наличността' }}
</button>
<StockWatcher v-if="watching" :product="PRODUCT" />
</main>
</template>
StockWatcher.vue<script setup>
import { onMounted, onUnmounted } from 'vue';
const props = defineProps(['product']);
let id;
onMounted(() => {
let checks = 0;
id = setInterval(() => {
checks++;
console.log(`Проверка №${checks}: ${props.product.name}`);
}, 1000);
});
onUnmounted(() => clearInterval(id));
</script>
<template>
<p class="watcher" role="status">
Следим наличността на {{ product.name }}. Ще ти кажем, щом се появи.
</p>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
Таймерът отива в onMounted, а onUnmounted(() => clearInterval(id)) го спира. id е let извън двете. Проверката хваща const id = setInterval(…) вътре в onMounted: това е ново id, видимо само там. Външното остава undefined и clearInterval(undefined) не спира нищо.
Идеята
Зависимостите
Сега купувачът може да избере друг изчерпан продукт, докато банерът е отворен. Ефектът проверява product. Щом продуктът се смени, старата проверка трябва да спре, а нова да тръгне за новия продукт.
Затова е watch. Той следи източник и вика функцията си, когато източникът се смени:
watch(() => props.product, (product, oldProduct, onCleanup) => { const id = setInterval(check, 1000); onCleanup(() => clearInterval(id));}, { immediate: true });- Източникът е функция, която връща стойността:
() => props.product. Vue вижда какво чете тя и знае кога да я извика наново. { immediate: true }пуска функцията и веднага, за началния продукт. Без негоwatchчака първата смяна.onCleanupзаписва почистването на това пускане. Vue го вика преди следващото пускане и когато компонентът си отиде. Наблюдателите от<script setup>спират сами с компонента.
watch сменя двете функции от миналата задача: onMounted пуска веднъж, а watch пуска при всеки нов продукт.
Виж го
Виж живота на един ефект
Банерът се отваря за баницата, купувачът избира краставиците, после затваря банера.
<script setup>const props = defineProps(['product']);
watch(() => props.product, (product, old, onCleanup) => { let checks = 0; const id = setInterval(() => { checks++; console.log(`Проверка №${checks}: ${product.name}`); }, 1000); onCleanup(() => clearInterval(id));}, { immediate: true });</script>
<template> <p role="status">Следим наличността на {{ product.name }}</p></template>Стъпка по стъпка, с думи
- Рисуване: Vue строи разметката от шаблона. Шаблонът само описва какво да се покаже. Не пуска нищо.
- immediate: true пуска наблюдателя веднъж веднага, още при създаването на компонента. Таймерът тръгва и брои проверките.
- Купувачът избира друг продукт. Функцията () => props.product вече връща друга стойност, затова наблюдателят трябва да тръгне пак.
- Преди новото пускане идва почистването на старото: неговият таймер спира. Без него старите проверки щяха да вървят до новите.
- Ефектът тръгва наново с новия продукт. Неговият таймер брои от №1.
- Купувачът затваря банера: компонентът излиза от страницата. Върви последното почистване и таймерът спира с него.
- Всяко пускане има своя двойка: каквото ефектът пусне, почистването му го спира. Преди следващото пускане и когато компонентът си отиде.
Ефектът тръгва два пъти и почистването върви два пъти: веднъж преди новото пускане, веднъж при махането. Между тях винаги работи точно един таймер.
Пробвай
Намери грешката
TypeError: Cannot read properties of undefined (reading 'name') на всяка секунда. Кой ред е виновен?Избери реда с грешката.
watch(props.product, …) дава на watch текущия продукт, а не източник. Vue не може да следи обикновен обект и казва [Vue warn]: Invalid watch source. Заради immediate функцията тръгва веднъж, но с undefined вместо продукт, и таймерът хвърля грешка на всяка секунда. Трябва функция: watch(() => props.product, …).Пробвай
Попълни
Какво стои на празното място, за да спре таймерът, когато компонентът си отиде?
let id;
onMounted(() => {
id = setInterval(check, 1000);
});
(() => clearInterval(id));onUnmounted: Vue вика функцията, когато компонентът излезе от страницата.Идеята
Кога не ти трябва ефект
Ефектът е за връзка с нещо извън компонента: таймер, сървър, заглавието на раздела, window. Две неща изглеждат като ефект, но не са.
- Стойност, изчислена от други. Общата сума на кошницата не се пази в отделно състояние, което ефект обновява. Тя се изчислява при рисуване с
computed, както в урока за изчисленото състояние. Ефект, който я пише в отделно състояние, е излишна работа и второ място, което може да се размине с кошницата. - Отговор на действие. Поръчката се праща, защото купувачът е натиснал „Поръчай“. Това е в обработчика на клика, а не в ефект, който гледа някакъв флаг.
Остава истинската работа на ефекта: да синхронизира компонента с външния свят, докато компонентът е на страницата.
По-точно, за любопитните
watchEffect. watchEffect((onCleanup) => { … }) тръгва веднага и следи всичко, което функцията му прочете. Не изреждаш източници, но и не виждаш с един поглед какво го пуска. watch с изричен източник е по-ясен, когато трябва да знаеш точно кога тръгва.
onWatcherCleanup. От Vue 3.5 почистването може да се запише и с onWatcherCleanup(() => …), внесено от 'vue'. То трябва да се извика преди първия await във функцията.
Кога тръгва immediate. Функцията с { immediate: true } тръгва още при създаването на компонента, преди той да е в DOM-а. Таймерът и заявката нямат нужда от DOM-а. Ако ефектът трябва да пипа елементи от шаблона, мястото е onMounted.
Options API има същото: mounted, unmounted и опцията watch.
Упражнение
Друг продукт
Над банера има падащо меню „Продукт“ с баницата и краставиците. Сега StockWatcher пуска таймера веднъж, при показване. Направи така, че при смяна на продукта старата проверка да спре, а нова да тръгне от №1 за новия продукт. Проверката отваря банера, чака „Проверка №1“ за баницата, избира краставиците, чака „Проверка №1“ за тях, гледа, че „Проверка №2“ за баницата не идва, и затваря банера.
Конзола
Решение
App.vue<script setup>
import { ref, computed } from 'vue';
import StockWatcher from './StockWatcher.vue';
const PRODUCTS = [
{ id: 4, name: 'Баница със сирене' },
{ id: 13, name: 'Краставици' },
];
const productId = ref(4);
const watching = ref(false);
const product = computed(() => PRODUCTS.find((p) => p.id === productId.value));
</script>
<template>
<main>
<label>
Продукт
<select :value="productId" @change="productId = Number($event.target.value)">
<option v-for="p in PRODUCTS" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</label>
<h1>{{ product.name }}</h1>
<p>Изчерпан. Ще се появи отново скоро.</p>
<button type="button" @click="watching = !watching">
{{ watching ? 'Спри следенето' : 'Следи наличността' }}
</button>
<StockWatcher v-if="watching" :product="product" />
</main>
</template>
StockWatcher.vue<script setup>
import { watch } from 'vue';
const props = defineProps(['product']);
watch(() => props.product, (product, oldProduct, onCleanup) => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Проверка №${checks}: ${product.name}`);
}, 1000);
onCleanup(() => clearInterval(id));
}, { immediate: true });
</script>
<template>
<p class="watcher" role="status">
Следим наличността на {{ product.name }}. Ще ти кажем, щом се появи.
</p>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
onMounted и onUnmounted се сменят с watch(() => props.product, (product, oldProduct, onCleanup) => { … }, { immediate: true }), а таймерът се спира с onCleanup. Проверката хваща watch без immediate (първата проверка не идва), watch(props.product, …) (невалиден източник) и watch без onCleanup (двата таймера вървят заедно).
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Props: данни отгоре · Условия: празно, грешка, зарежда · Състояние · Изчислено състояние
- Продължава в: Данни от API в компонент · Бързина: кога се рисува наново · Тестове на компоненти · От пясъчника към проект
- Задачи по темата: Ефекти, данни и формуляри