Средний 9 мин чтения
Система реактивности Vue
Vue автоматически отслеживает зависимости и обновляет DOM при изменении данных. Понимание реактивности помогает писать эффективные компоненты.
reactivityrefreactiveProxyVue 3
Как работает реактивность в Vue 3
Vue 3 использует ES Proxy для отслеживания изменений. При доступе к реактивному свойству Vue «подписывается» на него, при изменении — обновляет все зависимые эффекты (рендер, computed, watch).
import { reactive, effect } from 'vue'
const state = reactive({ count: 0 })
// effect() запускается при каждом изменении зависимостей
effect(() => {
console.log('count:', state.count) // → 0
})
state.count++ // эффект запустится снова → 'count: 1'
ref под капотом
ref() создаёт объект-обёртку { value: T }, где value является реактивным:
import { ref } from 'vue'
const count = ref(0)
// По сути: { value: 0 } (объект отслеживается через Proxy)
// В шаблоне Vue автоматически разворачивает .value
// {{ count }} работает как {{ count.value }}
Потеря реактивности
Деструктуризация reactive
import { reactive } from 'vue'
const state = reactive({ x: 0, y: 0 })
// ❌ Деструктуризация разрывает связь с Proxy
const { x, y } = state
x++ // state.x НЕ изменится, реактивность потеряна
// ✅ Используйте toRefs()
import { toRefs } from 'vue'
const { x, y } = toRefs(state)
x.value++ // state.x изменится, реактивность сохранена
Переприсваивание reactive
let state = reactive({ count: 0 })
// ❌ Переприсваивание уничтожает реактивную ссылку
state = reactive({ count: 1 }) // компонент не обновится!
// ✅ Изменяйте свойства, не саму переменную
state.count = 1
Object.assign(state, { count: 1 })
ref в reactive
const count = ref(0)
const state = reactive({ count })
// count автоматически разворачивается в reactive
state.count++ // увеличивает count.value
console.log(count.value) // 1
toRef и toRefs
import { reactive, toRef, toRefs } from 'vue'
const user = reactive({ name: 'Иван', age: 25 })
// toRef — одно свойство
const name = toRef(user, 'name')
name.value = 'Пётр' // user.name изменится
// toRefs — все свойства
const { name, age } = toRefs(user)
name.value = 'Пётр'
age.value = 26
shallowRef и shallowReactive
Для производительности — отслеживают только верхний уровень:
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// Только .value отслеживается, не содержимое объекта
const state = shallowRef({ items: [], count: 0 })
state.value.count++ // ❌ не вызовет обновление
state.value = { ...state.value, count: state.value.count + 1 } // ✅
// Или принудительно запустить обновление
triggerRef(state)
readonly
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const readonlyState = readonly(state)
readonlyState.count++ // ⚠️ Warning: Set operation on key "count" failed: target is readonly
Вычисляемые свойства и ленивость
computed() — ленивое и кэширующее:
import { ref, computed } from 'vue'
const items = ref([1, 2, 3, 4, 5])
const expensiveFilter = computed(() => {
console.log('Пересчёт!')
return items.value.filter(n => n > 2)
})
// Первое обращение — вычисляется
console.log(expensiveFilter.value) // 'Пересчёт!' → [3, 4, 5]
// Повторное обращение без изменений — из кэша
console.log(expensiveFilter.value) // Без 'Пересчёт!'
// После изменения зависимости — пересчитывается
items.value.push(6) // отметит computed как «грязный»
console.log(expensiveFilter.value) // 'Пересчёт!' → [3, 4, 5, 6]
Паттерн: реактивные данные из API
<script setup>
import { ref, watch } from 'vue'
const userId = ref(1)
const user = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchUser(id) {
loading.value = true
error.value = null
try {
const res = await fetch(`/api/users/${id}`)
user.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch(userId, fetchUser, { immediate: true })
</script>
effectScope
Группировка эффектов для одновременной остановки:
import { effectScope, ref, watchEffect } from 'vue'
const scope = effectScope()
scope.run(() => {
const count = ref(0)
watchEffect(() => console.log(count.value))
// Все watchEffect/watch/computed внутри привязаны к scope
})
// Остановить все эффекты разом
scope.stop()