Junior: питання на співбесіді з теми «Реактивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Обидва створюють реактивний стан, за яким 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 вирішить його перерахувати.
У шаблоні .value писати не треба - але лише для ref верхнього рівня, тобто змінних, оголошених прямо в <script setup> (чи повернених з setup()).
<script setup>
import { ref } from 'vue';
const count = ref(1);
const item = { id: ref(1) }; // ref усередині звичайного об'єкта
</script>
<template>
<p>{{ count + 1 }}</p> <!-- 2 -->
<p>{{ item.id }}</p> <!-- 1 -->
<p>{{ item.id + 1 }}</p> <!-- [object Object]1 -->
</template>
Чому так:
count- ref верхнього рівня, у виразі він розгорнутий;item.id- ref усередині звичайного об'єкта. Виразitem.id + 1додає до об'єкта-ref число;{{ item.id }}без обчислень показує1- це окреме правило: якщо кінцевий результат інтерполяції - ref, текст розгортається. Але для будь-якого виразу з ним - ні.
Як виправити:
const { id } = item; // зробити ref верхнього рівня
або явно {{ item.id.value + 1 }}.
Інші правила розгортання:
- у
reactive()ref, що лежить як властивість, розгортається при доступі:state.count, а неstate.count.value; - у масивах і колекціях (
reactive([ref(1)]),Map) - не розгортається, потрібно.value; - props приходять уже розгорнутими значеннями -
.valueне потрібен.
Практичне правило, що знімає плутанину: не класти ref-и всередину звичайних об'єктів. Або окремі ref-и на верхньому рівні, або один reactive/ref з об'єктом, поля якого - звичайні значення.
Інструменти допомагають: Vue - Official (Volar) у редакторі підсвічує, де .value потрібен, а TypeScript покаже, що item.id + 1 додає Ref<number> до числа.
Докладніше в документації: Особливість розгортання в шаблонах
За замовчуванням computed - лише для читання: значення виводиться з інших даних. Спроба записати дає попередження в консолі, і нічого не змінюється.
Записувана обчислювана властивість - об'єкт з get і set:
import { ref, computed } from 'vue';
const firstName = ref('Оля');
const lastName = ref('Коваль');
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
[firstName.value, lastName.value] = value.split(' ');
},
});
fullName.value = 'Іра Лі';
firstName.value; // 'Іра'
set не зберігає значення сам - він змінює вихідні дані, з яких get потім перерахує результат.
Типові застосування:
1. v-model над перетвореним значенням - наприклад, ціна в копійках у стані й у гривнях у полі:
const priceCents = ref(12550);
const priceUah = computed({
get: () => (priceCents.value / 100).toFixed(2),
set: (value) => { priceCents.value = Math.round(Number(value) * 100); },
});
<input v-model="priceUah" type="number" step="0.01">
2. Обгортка над props + emit для v-model на компоненті (до Vue 3.4 - стандартний шаблон, тепер його замінює defineModel):
const value = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
3. Синхронізація з URL чи сховищем: читання з параметра запиту, запис - через router.replace.
Що варто знати:
getмає бути чистим: без побічних ефектів, запитів, змін іншого стану;setне викликається, якщо змінити значення «всередині»: для об'єктаfullName.value.x = 1змінює повернений об'єкт, а не викликає сеттер;- у Vue 3.4+
getотримує попереднє значення першим аргументом - зручно, щоб повернути той самий об'єкт, якщо нічого суттєво не змінилося, і не запускати зайві оновлення.
Докладніше в документації: Обчислювані властивості з записом
Vue оновлює шаблон лише тоді, коли змінюється реактивний стан - той, за яким система реактивності може стежити. Звичайна змінна такою не є.
<script setup>
let count = 0; // звичайна змінна
const increment = () => count++;
</script>
<template>
<button @click="increment">{{ count }}</button> <!-- завжди 0 -->
</template>
count справді збільшується в пам'яті, але Vue про це не знає й не перемальовує компонент.
Як правильно:
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>
Чому Vue не може стежити за звичайною змінною: JavaScript не дає способу перехопити присвоєння локальній змінній. Реактивність працює через перехоплення доступу до властивостей: ref - це об'єкт з властивістю value, reactive - проксі над об'єктом. Читання реєструє залежність, запис - сповіщає про зміну.
Коли звичайна змінна доречна:
- константи й дані, які не змінюються (
const MAX_ITEMS = 50); - функції-обробники;
- значення, що потрібні лише в логіці й не показуються в шаблоні (ідентифікатор таймера, екземпляр сторонньої бібліотеки).
Схожі пастки:
- заміна
reactive-об'єкта цілком:let state = reactive({...}); state = reactive({...})- шаблон стежить за старим проксі; - деструктуризація
reactiveчи props - отримані змінні не реактивні; - дані, отримані з
fetch, записані в звичайну змінну замістьref- перший рендер порожній, і після завантаження нічого не змінюється.
Підказка: якщо значення має з'являтися в шаблоні й змінюватися - воно має бути ref, reactive чи computed.