Ref проти reactive у Vue.js
ref проти reactive у Vue.js: ref обгортає будь-яке значення в реактивний контейнер з доступом через .value; reactive перетворює об'єкт на глибокий проксі з прямим доступом до властивостей.
Теорія
TL;DR
ref= коробка навколо значення; в скрипті дістаєш через.value, у шаблоні розпаковується автоматичноreactive= живий проксі об'єкта; звертаєшся до властивостей напряму, без обгорткиrefприймає примітиви і об'єкти;reactiveтільки об'єкти (на примітивах кидає помилку)- Треба повністю замінити значення? Тільки
refдозволяє це безпечно - Не знаєш що вибрати? Команда Vue зараз рекомендує
refяк дефолт
Швидкий приклад
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0) // примітив: потрібен .value у скрипті
count.value++ // count.value тепер 1
const state = reactive({ count: 0, user: 'Alice' }) // об'єкт: прямий доступ
state.count++ // state.count тепер 1
</script>
<template>
<p>{{ count }}</p> <!-- автоматично розпаковується: показує 1, без .value -->
<p>{{ state.count }}</p> <!-- прямий доступ: показує 1 -->
</template>Vue розпаковує ref у шаблонах автоматично. Пишеш {{ count }}, а не {{ count.value }}. У скрипті .value завжди потрібен.
Головна відмінність
ref додає тонку обгортку навколо будь-якого значення. Примітиви стають реактивними через цю обгортку, об'єкти отримують проксі всередині неї. Обгортка дає стабільне посилання, яке можна переназначити. reactive обгортки немає, тому отримуєш зручну крапкову нотацію на об'єктах, але замінити весь об'єкт не вийде. Переназначиш змінну з reactive і проксі зникне. Шаблон перестане оновлюватись.
Коли що використовувати
- Лічильник, boolean, рядок →
ref. Просто і можна переназначити. - Форма з вкладеними полями →
reactive, якщо ніколи не замінюєш форму цілком;ref, якщо скидаєш через новий об'єкт. - Відповідь від API →
ref. Значення може бутиnullдо запиту, а після - об'єктом або масивом. - Масив елементів →
ref. Можеш замінити весь масив або додавати черезpush. - Не знаєш що вибрати? Багато команд використовують
refдля всього і додаютьreactiveтільки коли крапкова нотація реально спрощує код.
Порівняльна таблиця
| Особливість | ref | reactive |
|---|---|---|
| Вхідні типи | Примітив або об'єкт | Тільки об'єкт / масив |
| Доступ у скрипті | Потрібен .value | Прямий доступ до властивостей |
| Доступ у шаблоні | Автоматично розпаковується | Прямий доступ до властивостей |
| Повне переназначення | Так (ref.value = newObj) | Ні (ламає проксі) |
| Примітиви | Так (ref(42)) | Ні (кидає помилку) |
| Деструктуризація | Безпечна (ref залишається реактивним) | Втрачає реактивність; потрібен toRefs |
| Тип у TypeScript | Ref<T> | Оригінальний тип |
| Коли брати | Лічильники, динамічні значення, будь-який примітив | Об'єкт фіксованої структури (якщо не потрібен повний reset) |
Як це працює всередині
Обидва - ref і reactive - побудовані на ES6 Proxy. reactive обгортає об'єкт у Proxy напряму, перехоплюючи get і set на кожному вкладеному рівні. ref створює маленький об'єкт-обгортку з get/set на .value, який далі використовує ту саму систему проксі. Коли пишеш {{ count }} у шаблоні, компілятор Vue викликає unref(count): якщо у значення є прапорець __v_isRef, читається .value; інакше значення повертається як є.
Типові помилки
reactive на примітиві:
const count = reactive(0) // Помилка: reactive() must be called on an objectРішення: const count = ref(0).
Забули .value у скрипті:
const count = ref(0)
count++ // Неправильно: змінюється посилання на обгортку, а не внутрішнє значення
// count.value залишається 0Рішення: count.value++ або ++count.value.
Переназначення змінної з reactive:
let state = reactive({ count: 0 })
state = { count: 10 } // Проксі зник. Шаблон більше не оновлюється.Рішення: мутуй властивості напряму (state.count = 10) або перейди на ref.
Деструктуризація reactive без toRefs:
const state = reactive({ name: 'Alice', age: 25 })
let { name } = state // name тепер звичайний рядок, не реактивний
name = 'Bob' // шаблон не оновитьсяРішення: const { name } = toRefs(state), потім name.value = 'Bob'.
Де це зустрічається в реальних проектах
- Pinia: часто використовує
reactiveдля кореневого об'єкта стану (фіксована структура, прямі мутації) - Nuxt
useFetch: повертаєdataякref, бо до запиту значенняnull, а потім об'єкт або масив - VueUse: майже всі composables використовують
refвсередині (useStorage,useLocalStorageтощо) - VeeValidate / FormKit:
reactiveдля об'єктів форм з вкладеним станом полів - Стан компонента: на практиці більшість команд, що починають мішати обидва підходи, через якийсь час стандартизуються на
ref- він покриває всі випадки без пастки з переназначенням
Додаткові питання
Q: Чому переназначення змінної з reactive ламає реактивність?
A: Тому що reactive() повертає Proxy, прив'язаний до конкретного об'єкта. Коли пишеш state = {}, ти відпускаєш посилання на цей Proxy. Трекер залежностей Vue все ще вказує на старий проксі, тому оновлення не спрацьовують.
Q: Як шаблон розпаковує ref автоматично?
A: Компілятор шаблонів Vue обгортає ref верхнього рівня через unref() у згенерованій render-функції. Якщо у значення є прапорець __v_isRef, читається .value; інакше значення повертається як є.
Q: Чи можна вкласти ref всередину reactive?
A: Так. reactive({ count: ref(0) }) працює, і Vue автоматично розпаковує вкладений ref, тому state.count доступний без .value. Але це ускладнює код - краще дотримуватись одного підходу.
Q: Що таке shallowRef і shallowReactive?
A: Обидва пропускають глибоку реактивність. shallowRef реагує тільки на переназначення .value, а не на зміни вкладених властивостей. shallowReactive відстежує тільки властивості верхнього рівня. Корисно для великих об'єктів, де глибоке відстеження занадто затратне.
Q: Є різниця в продуктивності для великих об'єктів?
A: Один reactive-проксі швидший за багато окремих ref-обгорток при тисячах реактивних властивостей. Різниця відчутна тільки при такому масштабі - у типовому стані компонента вона несуттєва.
Приклади
Базовий: лічильник і стан форми
<script setup>
import { ref, reactive } from 'vue'
// ref для простого лічильника
const count = ref(0)
// reactive для форми (об'єкт тільки мутується, ніколи не замінюється цілком)
const form = reactive({
name: '',
email: '',
preferences: { theme: 'light', notifications: true }
})
function resetForm() {
form.name = ''
form.email = ''
form.preferences.theme = 'light'
// form = {} тут не спрацює - це видалить проксі
}
</script>
<template>
<button @click="count++">Кількість: {{ count }}</button>
<form @submit.prevent>
<input v-model="form.name" placeholder="Ім'я" />
<input v-model="form.email" placeholder="Email" />
<label>
<input type="checkbox" v-model="form.preferences.notifications" />
Сповіщення
</label>
</form>
<button @click="resetForm">Скинути</button>
</template>count використовує ref, бо це число з .value у скрипті. form використовує reactive, бо це об'єкт, який мутується на місці і ніколи не замінюється цілком.
Граничний випадок: переназначення і втрата реактивності
Це класика, яка плутає розробників на переході з Vue 2 або тих хто мішає обидва API в одному компоненті.
<script setup>
import { ref, reactive } from 'vue'
const objRef = ref({ count: 0 })
const objReactive = reactive({ count: 0 })
// ref: переназначення .value створює новий проксі, реактивність збережена
objRef.value = { count: 10 }
console.log(objRef.value.count) // 10 - шаблон оновлюється коректно
// reactive: переназначення змінної видаляє проксі повністю
// objReactive = { count: 10 } // JS виконається, але Vue втрачає зв'язок
// Безпечний спосіб оновити reactive - мутуй властивості
Object.assign(objReactive, { count: 10 }) // Правильно
console.log(objReactive.count) // 10 - шаблон все ще оновлюється
</script>ref контролює проксі через .value. reactive і є проксі. Заміниш змінну - проксі зникне.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.