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

Ефекти

Накратко: Ефектът е код за страничните действия (таймер, заявка, заглавие на раздела); във Vue onMounted го пуска, когато компонентът е на страницата, watch при смяна на източника, а почистването спира пуснатото.

Проблем

Банерът си отиде, проверките остават

Баницата със сирене е изчерпана. Купувачът натиска „Следи наличността“ и под бутона се появява банер. Докато банерът е отворен, страницата проверява наличността на всяка секунда. Тук проверката само пише ред в конзолата.

Vue

Натисни „Следи наличността“, изчакай две секунди и натисни „Спри следенето“. Банерът изчезва, а в конзолата пак идват „Проверка №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“ не идва.

Vue

Идеята

Зависимостите

Сега купувачът може да избере друг изчерпан продукт, докато банерът е отворен. Ефектът проверява 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>
Стъпка по стъпка, с думи
  1. Рисуване: Vue строи разметката от шаблона. Шаблонът само описва какво да се покаже. Не пуска нищо.
  2. immediate: true пуска наблюдателя веднъж веднага, още при създаването на компонента. Таймерът тръгва и брои проверките.
  3. Купувачът избира друг продукт. Функцията () => props.product вече връща друга стойност, затова наблюдателят трябва да тръгне пак.
  4. Преди новото пускане идва почистването на старото: неговият таймер спира. Без него старите проверки щяха да вървят до новите.
  5. Ефектът тръгва наново с новия продукт. Неговият таймер брои от №1.
  6. Купувачът затваря банера: компонентът излиза от страницата. Върви последното почистване и таймерът спира с него.
  7. Всяко пускане има своя двойка: каквото ефектът пусне, почистването му го спира. Преди следващото пускане и когато компонентът си отиде.

Ефектът тръгва два пъти и почистването върви два пъти: веднъж преди новото пускане, веднъж при махането. Между тях винаги работи точно един таймер.

Пробвай

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

Банерът се отваря, но вместо проверки конзолата пише предупреждение и TypeError: Cannot read properties of undefined (reading 'name') на всяка секунда. Кой ред е виновен?

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

Пробвай

Попълни

Какво стои на празното място, за да спре таймерът, когато компонентът си отиде?

let id;
onMounted(() => {
  id = setInterval(check, 1000);
});
(() => clearInterval(id));

Идеята

Кога не ти трябва ефект

Ефектът е за връзка с нещо извън компонента: таймер, сървър, заглавието на раздела, 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“ за баницата не идва, и затваря банера.

Vue

Тест

Провери се

1. Компонентът пуска setInterval и не го спира. Купувачът отваря и затваря банера три пъти. Колко таймера вървят?
2. Наблюдателят е watch(() => props.product, …), без { immediate: true }. Кога тръгва първата проверка?
3. Кога върви почистването на ефекта?
4. Кое от тези НЕ е работа за ефект?

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