Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює реактивність у Vue.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Реактивність у Vue.js** відстежує, які дані компонент зчитує під час рендеру, через ES6 `Proxy` і повторно запускає тільки залежні компоненти. ```js const count = ref(0) // ref для примітивів - доступ через .value const state = reactive({ n: 0 }) // reactive для об'єктів - прямий доступ count.value++ // спрацьовує setter Proxy, залежні шаблони ре-рендеряться state.n++ // той самий механізм Proxy ``` **Головне:** `ref()` для примітивів, `reactive()` для об'єктів. `Proxy` у Vue 3 перехоплює будь-яку зміну включно з динамічно доданими властивостями; `Object.defineProperty` у Vue 2 - ні.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Реактивність у 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` повертає кешований результат без повторного запуску фільтра.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.