Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Vue: реактивність і компоненти

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

ref і reactive, computed і watch, composables, пропси, події й v-model, слоти, provide/inject, хуки життєвого циклу - питання всіх рівнів, від junior до senior.

За спробу
20
У пулі
72
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

61 питання

Обидва створюють реактивний стан, за яким Vue стежить і перемальовує шаблон при змінах.

  • ref(value) - обгортка над будь-яким значенням: числом, рядком, об'єктом. Значення лежить у .value. У шаблоні .value писати не треба - Vue розгортає ref автоматично.
  • reactive(object) - реактивний проксі над об'єктом (чи масивом, Map). Поля читаються напряму, без .value.
import { ref, reactive } from 'vue';

const count = ref(0);
count.value++;

const form = reactive({ email: '', password: '' });
form.email = 'olia@example.com';

Обмеження reactive:

  • Працює лише з об'єктами - reactive(0) не має сенсу.
  • Не можна замінити цілком: form = reactive({...}) розриває зв'язок із шаблоном, бо шаблон стежить за старим проксі.
  • Деструктуризація губить реактивність: const { email } = form дає звичайний рядок.

Через ці обмеження документація Vue радить ref як основний інструмент: його можна передавати у функції, повертати з composables, перезаписувати цілком (items.value = newItems), і поведінка завжди однакова. reactive зручний для згрупованого стану на кшталт форми, який не замінюють і не деструктуризують.

Докладніше в документації: Основи реактивності

computed - значення, похідне від іншого стану. Vue кешує результат і перераховує його, лише коли змінилися реактивні залежності.

const items = ref([]);
const total = computed(() => items.value.reduce((sum, i) => sum + i.price, 0));

Метод у шаблоні ({{ calcTotal() }}) викликається при кожному рендері компонента, навіть якщо items не змінювалися. Для важких обчислень це зайва робота.

watch - не значення, а побічна дія у відповідь на зміну: запит на сервер, запис у localStorage, ручна робота з DOM.

watch(searchQuery, async (query) => {
  results.value = await search(query);
});

Як обирати:

  • Потрібно значення для шаблону чи коду - computed.
  • Потрібно щось зробити, коли значення змінилося, - watch.
  • Потрібна дія з аргументами за запитом (обробник кліку) - метод.

Типова помилка - watch, що просто записує похідне значення в інший ref:

watch(items, () => { total.value = sum(items.value); }, { deep: true }); // зайве

Це той самий computed, але з ручною синхронізацією, яку легко зламати.

computed має бути чистим: без запитів, змін іншого стану чи DOM. Інакше результат залежатиме від того, коли й скільки разів Vue вирішить його перерахувати.

Докладніше в документації: Обчислювані властивості

  • 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.5 deep приймає й число - глибину обходу.
  • 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.

Докладніше в документації: Обмеження reactive()

Основа - Proxy і два хуки: track і trigger.

  • reactive() загортає об'єкт у Proxy. Обробник get викликає track: запам'ятовує, що поточний «ефект» (рендер компонента, computed, watch) читає це поле.
  • Обробник set викликає trigger: знаходить усі ефекти, що читали поле, і планує їх перезапуск.
  • ref робить те саме через геттер і сеттер на .value - тому він працює з примітивами, які проксувати неможливо.
// спрощено
const state = new Proxy(target, {
  get(obj, key) { track(obj, key); return obj[key]; },
  set(obj, key, value) { obj[key] = value; trigger(obj, key); return true; },
});

Рендер компонента - теж ефект: під час рендеру він «підписується» рівно на ті поля, які прочитав шаблон. Зміна поля, якого шаблон не читає, рендер не запускає.

Планувальник і nextTick. Зміни не перемальовують DOM одразу. Vue збирає всі ефекти, які треба перезапустити, у чергу й виконує їх один раз в мікрозадачі. Десять змін поспіль - один рендер.

Звідси nextTick: одразу після зміни стану DOM ще старий.

items.value.push(newItem);
await nextTick();
listEl.value.lastElementChild.scrollIntoView(); // DOM уже оновлено

Наслідки для продуктивності:

  • Глибока реактивність проксує вкладені об'єкти ліниво, при доступі. Великі незмінні дані (результат API на тисячі рядків) дешевше тримати в shallowRef, де відстежується лише заміна .value.
  • markRaw позначає об'єкт, який Vue ніколи не має робити реактивним: екземпляри сторонніх бібліотек, карти, редактори.

Докладніше в документації: Реактивність у деталях

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.