Як працює реактивність у Vue.js?
Реактивність у Vue.js автоматично відстежує, які дані компонент зчитує під час рендеру, і повторно запускає тільки ті частини, що залежать від даних які змінились.
Теорія
TL;DR
- Аналогія: формула в Excel. Змінюєш одну клітинку, всі формули що посилаються на неї перераховуються самі.
- Механізм:
Proxyперехоплює читання і запис властивостей, будує граф залежностей під час рендеру, запускає тільки потрібні компоненти. - Vue 3 використовує ES6
Proxy; Vue 2 використовувавObject.definePropertyі пропускав динамічно додані властивості. - Правило вибору:
ref()для примітивів і простих значень;reactive()для складних вкладених об'єктів. - Шаблони автоматично розгортають
ref, тому.valueв HTML не потрібен.
Швидкий приклад
<script setup>
import { ref } from 'vue'
const count = ref(0) // Реактивний примітив - загорнутий у .value
const increment = () => count.value++ // .value активує setter Proxy
</script>
<template>
<!-- Шаблон автоматично розгортає ref - .value тут не потрібен -->
<button @click="increment">Кліків: {{ count }}</button>
</template>Коли натискаєш кнопку, count.value++ потрапляє в setter Proxy. Vue знаходить всі шаблони і computed що зчитували count під час останнього рендеру і повторно їх запускає. Нічого зайвого не чіпається.
Як Proxy відстежує залежності
Під час рендеру Vue загортає дані в ES6 Proxy. Кожного разу коли шаблон або computed зчитує властивість, Proxy викликає track(), який реєструє: "цей ефект залежить від цієї властивості". Кожен запис викликає trigger(), який ставить у чергу повторний рендер для всіх зареєстрованих ефектів.
Залежності зберігаються у структурі WeakMap<object, Map<key, Set<effect>>>. Завдяки цьому Vue точно знає, які ефекти перезапустити для конкретної зміни властивості, не зачіпаючи непов'язаний стан. Вся система працює на звичайному браузерному JS, без жодних рантайм-хуків V8.
У Vue 2 те саме робилось через геттери і сеттери Object.defineProperty. Це працювало для властивостей, визначених заздалегідь, але пропускало нові властивості додані пізніше і присвоювання за індексом масиву (arr[0] = x). Proxy у Vue 3 покриває все це автоматично.
Головна різниця між ref() і reactive()
ref() загортає будь-яке значення (примітив або об'єкт) у контейнер з властивістю .value. Proxy сидить на цьому контейнері. reactive() перетворює весь об'єкт на Proxy напряму, тому звертатись до властивостей можна без .value.
Обидва дають глибоку реактивність для вкладених об'єктів. Практичне розмежування: ref для речей що можеш повністю замінити (count.value = 0), reactive для об'єктів де мутуєш властивості на місці.
Коли що використовувати
- Примітив (число, рядок, булеве):
ref()- єдиний варіант що працює. - Значення яке можеш повністю замінити:
ref()-count.value = newCountвиглядає чисто. - Складний вкладений об'єкт з багатьма властивостями:
reactive()- без зайвого.value. - Похідні дані що мають кешуватись:
computed()- перераховує тільки коли залежності змінились. - Async або зовнішні дані:
ref()плюсawaitприродно обробляє неініціалізований стан. - Об'єкти що не потрібно відстежувати:
markRaw()- вимкнути реактивність повністю.
Типові помилки
1. Push звичайного об'єкта у реактивний масив
// Неправильно - новий об'єкт не є реактивним
state.items.push({ count: 0 })
state.items[0].count++ // UI не оновлюється
// Правильно - загорнути в reactive()
state.items.push(reactive({ count: 0 }))
state.items[0].count++ // Оновлюється коректноЗовнішній масив є Proxy, але об'єкт що ти запушив - звичайний JS-об'єкт. Мутації його властивостей не мають Proxy який би їх перехопив.
2. Забути .value у скрипті
const count = ref(0)
count++ // Неправильно - переприсвоюєш посилання на об'єкт Ref
count.value++ // ПравильноШаблони розгортають автоматично, скрипти - ні. Це розходження ловить майже всіх хоча б раз, особливо тих хто приходить з React де useState повертає значення напряму.
3. Заміна всього реактивного масиву
// Неправильно - руйнує ланцюг Proxy
state.arr = []
// Правильно - мутувати на місці
state.arr.length = 0
// або
state.arr.splice(0)Переприсвоєння створює новий звичайний масив і скидає Proxy. Все що відстежувало старе посилання перестає оновлюватись.
4. Деструктуризація реактивного об'єкта
const state = reactive({ count: 0 })
const { count } = state // count тепер звичайне число - не реактивне
// Правильно - використати toRefs()
import { toRefs } from 'vue'
const { count } = toRefs(state) // count тепер ref - реактивнийЦе найпоширеніша пастка з reactive(). Щойно витягуєш властивість, губиш зв'язок з Proxy.
5. Індекс масиву як ключ у v-for
<!-- Неправильно - переупорядкування DOM ламає відстеження залежностей -->
<li v-for="(item, i) in list" :key="i">
<!-- Правильно - стабільний ідентифікатор -->
<li v-for="item in list" :key="item.id">Де зустрічається в реальних проектах
- Nuxt.js:
useState()загортаєreactiveдля SSR-безпечного спільного стану між компонентами. - Pinia:
defineStoreвикористовуєrefіreactiveяк основу всіх сторів. - VueUse:
useStorageтримаєrefсинхронізованим зlocalStorageреактивно. - Quasar: валідація форм через реактивні об'єкти помилок на
reactive. - VitePress: реактивні фільтри пошуку по статичному markdown-контенту через
computed.
Питання на співбесіді
Q: Яка різниця між ref() і reactive()?
A: ref() загортає будь-яке значення у контейнер .value і працює з примітивами. reactive() проксує об'єкт напряму без .value. Обидва глибоко реактивні. Головне: ref коли можеш повністю замінити значення, reactive коли мутуєш властивості на місці.
Q: Як Vue виявляє зміни в масивах?
A: Proxy перехоплює встановлення length і запис за індексом напряму. Методи push, pop, splice мутують проксований масив, тому Proxy перехоплює кожну операцію і запускає залежності.
Q: Що станеться якщо присвоїти нереактивний об'єкт реактивній властивості?
A: Значення властивості стає звичайним об'єктом. Мутації його вкладених властивостей не мають Proxy що їх перехопив би, тому UI не оновиться. Рішення: загорнути присвоюване значення в reactive() перед присвоєнням.
Q: Чому Vue 3 перейшов від Object.defineProperty до Proxy?
A: Object.defineProperty загортає тільки властивості що існували на момент ініціалізації. Нова властивість додана пізніше, або запис за індексом масиву (arr[0] = x), обходили сеттер повністю. Proxy загортає сам об'єкт, тому будь-яка операція включно з новими властивостями, видаленнями і записом за індексом перехоплюється автоматично.
Q: Як дебажити втрату реактивності у великому продакшен-застосунку?
A: Починай з Vue DevTools і перевіряй стан компонента. Якщо значення там є але UI не оновлюється, посилання в шаблоні застаріле. Перевір деструктуровані реактивні об'єкти (чи є toRefs). Поглянь на виклики markRaw() що могли вимкнути реактивність спільного об'єкта. Timeline у Vue DevTools показує які ефекти спрацювали і звужує де саме обірвався ланцюг залежностей.
Приклади
Базовий: лічильник з ref
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count.value++">Натиснуто {{ count }} разів</button>
</template>count починається з 0. Кожен клік інкрементує .value, спрацьовує setter Proxy, Vue ре-рендерить текст кнопки. Шаблон використовує {{ count }} без .value бо шаблони автоматично розгортають refs.
Середній: список завдань з вкладеним реактивним станом
<script setup>
import { reactive, ref } from 'vue'
const todos = reactive({ list: [], filter: 'all' })
const newTodo = ref('')
const addTodo = () => {
if (!newTodo.value.trim()) return
todos.list.push({ id: Date.now(), text: newTodo.value, done: false })
newTodo.value = ''
}
const toggle = (id) => {
const todo = todos.list.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
</script>
<template>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Нове завдання" />
<ul>
<li v-for="todo in todos.list" :key="todo.id">
<input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" />
<span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">
{{ todo.text }}
</span>
</li>
</ul>
</template>todos є реактивним об'єктом, тому todos.list, todos.filter і кожна властивість елементів всередині списку - всі проксовані. Додавання елемента оновлює список. Перемикання done закреслює текст. todo.id як ключ замість індексу запобігає застарілим DOM-посиланням при переупорядкуванні.
Просунутий: computed з кешуванням залежностей
<script setup>
import { reactive, computed } from 'vue'
const todos = reactive({
list: [
{ id: 1, text: 'Купити молоко', done: false },
{ id: 2, text: 'Написати тести', done: true }
]
})
// Перераховується тільки коли todos.list змінюється
const remaining = computed(() =>
todos.list.filter(t => !t.done).length
)
const completeAll = () => {
todos.list.forEach(t => { t.done = true })
}
</script>
<template>
<p>Залишилось {{ remaining }} завдань</p>
<button @click="completeAll">Завершити всі</button>
</template>computed() реєструє власний ефект під час обчислення. Перший раз коли remaining зчитується, він відстежує кожен доступ до властивостей всередині геттера. Після того як completeAll встановлює всі done в true, Vue запускає ефект remaining, перераховує значення і оновлює параграф. Якщо між рендерами нічого в todos.list не змінилось, computed повертає кешований результат без повторного запуску фільтра.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.