Middle: питання на співбесіді з теми «Реактивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
watch(source, callback)стежить за явно вказаним джерелом. Колбек отримує нове й старе значення і за замовчуванням не запускається одразу - лише при першій зміні.watchEffect(callback)запускається одразу і сам відстежує всі реактивні значення, які прочитав під час виконання. Старого значення немає.
watch(userId, async (id, oldId) => {
user.value = await fetchUser(id);
});
watchEffect(async () => {
user.value = await fetchUser(userId.value); // залежність від userId знайдена сама
});
Опції watch:
immediate: true- викликати колбек одразу з поточним значенням, якwatchEffect.deep: true- реагувати на зміни вкладених полів об'єкта, а не лише на заміну всього значення. Заreactive-об'єктомwatchстежить глибоко автоматично. З Vue 3.5deepприймає й число - глибину обходу.once: true(3.4+) - спрацювати лише раз.
Нюанси:
watchEffectвідстежує лише те, що прочитано синхронно до першогоawait. Значення, прочитані після, у залежності не потрапляють.deepна великій структурі - дорогий обхід при кожній зміні. Краще стежити за конкретним полем:watch(() => form.email, ...).- Гонки: якщо
userIdзмінився двічі швидко, відповіді можуть прийти не в тому порядку.onCleanup(чиonWatcherCleanupу 3.5) дозволяє скасувати попередній запит.
Правило: watch - коли важливо, що саме змінилося і нові/старі значення; watchEffect - коли ефект просто має відповідати поточному стану.
Реактивність Vue працює через проксі: Vue знає про зміну, бо зміна йде через проксі (state.count = 1). Деструктуризація копіює значення полів у звичайні змінні - зв'язок із проксі втрачається.
const state = reactive({ count: 0, name: 'Оля' });
const { count } = state; // просто число 0
state.count++; // count лишився 0
Як уникнути:
toRefsперетворює кожне поле наref, пов'язаний з початковим об'єктом:
const { count, name } = toRefs(state);
count.value++; // змінює state.count
toRef- для одного поля:const count = toRef(state, 'count').- Повертати з composables
ref-и, а неreactive, - тоді деструктуризація на місці виклику безпечна.
Props: props - теж реактивний об'єкт. До Vue 3.5 const { title } = defineProps(...) давав нереактивну змінну. З Vue 3.5 деструктуризація defineProps у <script setup> реактивна: компілятор сам перетворює title на props.title. Але передати таку змінну у функцію чи watch напряму не вийде - потрібен геттер: watch(() => title, ...).
Ще місця, де реактивність губиться:
- Заміна всього
reactive-об'єкта:state = reactive({...}). - Передача примітиву в функцію:
useSomething(state.count)передає число, а не реактивне джерело. ПередавайтеtoRef(state, 'count')чи геттер() => state.count.
Ці утиліти розв'язують одну проблему: як передати реактивне значення далі, не втративши реактивності.
toRefs(reactiveObj) - перетворює кожну властивість reactive-об'єкта на ref, пов'язаний з оригіналом. Головне застосування - повернути стан з composable так, щоб його можна було деструктуризувати:
function useMouse() {
const state = reactive({ x: 0, y: 0 });
// ...
return toRefs(state);
}
const { x, y } = useMouse(); // x і y - ref-и, реактивні
toRef - один ref:
const title = toRef(props, 'title'); // ref, що читає props.title
const fullName = toRef(() => `${props.first} ${props.last}`); // ref тільки для читання з геттера
toValue(source) (3.3+) - «розгорнути що завгодно»: ref - .value, геттер - результат виклику, звичайне значення - як є.
Composable, що приймає ref, геттер або значення - тип MaybeRefOrGetter:
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue';
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null);
watchEffect(async () => {
data.value = null;
const response = await fetch(toValue(url)); // читання всередині ефекту = залежність
data.value = await response.json();
});
return { data };
}
useFetch('/api/users'); // рядок
useFetch(userUrl); // ref
useFetch(() => `/api/users/${props.id}`); // геттер від props
Чому toValue треба викликати всередині watchEffect/computed: саме там Vue відстежує читання. Якщо розгорнути значення один раз на початку composable, зміни props.id вже не підхопляться.
Типова помилка з props:
useFetch(`/api/users/${props.id}`); // рядок обчислено один раз - не реактивно
useFetch(() => `/api/users/${props.id}`); // правильно
unref - старший аналог toValue, що не викликає геттери. У новому коді краще toValue.
До Vue 3.5 деструктуризація props губила реактивність: const { title } = defineProps(...) давала звичайне значення на момент створення компонента.
З Vue 3.5 деструктуризація з defineProps реактивна:
<script setup>
const { title, count = 0 } = defineProps(['title', 'count']);
</script>
<template>
<h1>{{ title }}</h1>
<p>{{ count }}</p>
</template>
Компілятор <script setup> переписує звернення title на __props.title, тож значення завжди актуальне. Заодно це найзручніший спосіб задати значення за замовчуванням - звичайним синтаксисом JavaScript замість withDefaults.
Пастка - передача у функцію чи watch:
const { id } = defineProps(['id']);
watch(id, () => { /* ... */ }); // помилка компіляції: передано значення, а не джерело
watch(() => id, () => { /* ... */ }); // правильно: геттер
useFetch(id); // composable отримає число, а не реактивне джерело
useFetch(() => id); // правильно
Деструктуризована змінна виглядає як звичайна змінна, але реактивна лише там, де компілятор може переписати звернення. Передаючи її кудись, треба загорнути в геттер, - інакше функція отримає «знімок» значення.
Спостереження за props без деструктуризації:
const props = defineProps(['id', 'filters']);
watch(() => props.id, (id) => load(id));
watch(() => props.filters, reload, { deep: true }); // об'єкт - глибоко
watch(props.id, ...) - та сама помилка: значення замість джерела.
Що варто знати:
- props змінювати не можна - вони належать батьківському компоненту. Для локальної копії -
ref(props.initial)(свідомо не реактивна до змін батька) абоcomputed; - об'єкти в props передаються за посиланням, і зміна їх полів технічно спрацює, але порушує односпрямований потік даних - батько не знатиме, хто змінив його стан;
- для
v-modelна компоненті -defineModel(), а не зміна props.
Типова задача: при зміні id завантажити дані. Якщо id змінюється швидко (користувач клацає по списку), запити летять паралельно, і відповідь на старий запит може прийти пізніше за новий - на екрані опиняться дані не того запису.
watch(id, async (newId) => {
const response = await fetch(`/api/users/${newId}`);
user.value = await response.json(); // може перезаписати свіжі дані старими
});
Очищення побічного ефекту. Перед наступним запуском спостерігача (і при його зупинці) Vue викликає функцію очищення - туди кладуть скасування попереднього запиту:
import { watch, onWatcherCleanup } from 'vue';
watch(id, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort()); // Vue 3.5+
try {
const response = await fetch(`/api/users/${newId}`, { signal: controller.signal });
user.value = await response.json();
} catch (error) {
if (error.name !== 'AbortError') throw error;
}
});
Важливо: onWatcherCleanup треба викликати синхронно, до першого await - після нього Vue вже не знає, до якого спостерігача він належить.
До Vue 3.5 - третій аргумент колбеку:
watch(id, async (newId, oldId, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
// ...
});
Цей варіант працює й зараз і не має обмеження щодо await.
Інші застосування очищення: зняти таймер (clearInterval), відписатися від WebSocket-каналу, знищити екземпляр сторонньої бібліотеки, створений для попереднього значення.
Зупинка спостерігача:
- спостерігачі, створені синхронно в
setup, зупиняються автоматично разом з компонентом; - створені асинхронно (після
awaitчи вsetTimeout) - ні: їх треба зупинити вручну, інакше витік пам'яті:
const stop = watch(source, callback);
stop();
У Vue 3.5 повернений об'єкт має ще й pause() і resume().
Альтернатива для даних, що залежать від параметрів, - бібліотеки на кшталт TanStack Query, що вже розв'язують гонитву, кешування й повтори.
Докладніше в документації: Спостерігачі: очищення побічних ефектів