Skip to main content

Як працює реактивність у Vue.js?

Реактивність у Vue.js автоматично відстежує, які дані компонент зчитує під час рендеру, і повторно запускає тільки ті частини, що залежать від даних які змінились.

Теорія

TL;DR

  • Аналогія: формула в Excel. Змінюєш одну клітинку, всі формули що посилаються на неї перераховуються самі.
  • Механізм: Proxy перехоплює читання і запис властивостей, будує граф залежностей під час рендеру, запускає тільки потрібні компоненти.
  • Vue 3 використовує ES6 Proxy; Vue 2 використовував Object.defineProperty і пропускав динамічно додані властивості.
  • Правило вибору: ref() для примітивів і простих значень; reactive() для складних вкладених об'єктів.
  • Шаблони автоматично розгортають ref, тому .value в HTML не потрібен.

Швидкий приклад

vue
<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 звичайного об'єкта у реактивний масив

js
// Неправильно - новий об'єкт не є реактивним 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 у скрипті

js
const count = ref(0) count++ // Неправильно - переприсвоюєш посилання на об'єкт Ref count.value++ // Правильно

Шаблони розгортають автоматично, скрипти - ні. Це розходження ловить майже всіх хоча б раз, особливо тих хто приходить з React де useState повертає значення напряму.

3. Заміна всього реактивного масиву

js
// Неправильно - руйнує ланцюг Proxy state.arr = [] // Правильно - мутувати на місці state.arr.length = 0 // або state.arr.splice(0)

Переприсвоєння створює новий звичайний масив і скидає Proxy. Все що відстежувало старе посилання перестає оновлюватись.

4. Деструктуризація реактивного об'єкта

js
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

html
<!-- Неправильно - переупорядкування 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

vue
<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.

Середній: список завдань з вкладеним реактивним станом

vue
<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 з кешуванням залежностей

vue
<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 повертає кешований результат без повторного запуску фільтра.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?