← Курс/ref() и reactive() в Vue 3#202 из 257+30 XP

ref() и reactive() в Vue 3

Две функции реактивности

Vue 3 предлагает два основных способа создать реактивные данные: ref() и reactive().

ref() — для примитивов и одиночных значений

ref() оборачивает значение в реактивный объект с полем .value:

<script setup>
import { ref } from 'vue'

const count = ref(0)
const name = ref('Алексей')
const isLoading = ref(false)
const items = ref([])   // ref может хранить и объекты/массивы

// Доступ к значению — через .value (в JS-коде)
console.log(count.value)   // 0
count.value++              // увеличить счётчик
count.value = 10           // установить значение

console.log(name.value)    // 'Алексей'
name.value = 'Борис'
</script>

<template>
  <!-- В шаблоне .value НЕ нужен — Vue разворачивает автоматически -->
  <p>{{ count }}</p>
  <p>{{ name }}</p>
</template>

reactive() — для объектов

reactive() делает объект реактивным без обёртки в .value:

<script setup>
import { reactive } from 'vue'

const user = reactive({
  name: 'Алексей',
  age: 25,
  address: {
    city: 'Москва'
  }
})

// Доступ напрямую, без .value
console.log(user.name)       // 'Алексей'
user.age++                   // изменение напрямую
user.address.city = 'СПб'   // вложенные объекты тоже реактивны

// Добавление новых свойств (Vue 3 — работает, в Vue 2 не работало)
user.email = 'alex@example.com'
</script>

<template>
  <p>{{ user.name }}, {{ user.age }} лет</p>
  <p>Город: {{ user.address.city }}</p>
</template>

Когда что использовать

| Ситуация | Рекомендация |

|---|---|

| Примитивы (число, строка, boolean) | ref() |

| Одна переменная | ref() |

| Группа связанных данных | reactive() |

| Массив | ref() (проще заменять целиком) |

| Форма с несколькими полями | reactive() |

На практике многие разработчики используют ref() для всего — это унифицированный подход.

Ограничения reactive()

reactive() имеет важное ограничение — **нельзя заменить объект целиком**:

import { reactive } from 'vue'

let state = reactive({ count: 0 })

// НЕ РАБОТАЕТ — реактивность теряется
state = { count: 1 }

// РАБОТАЕТ — изменяем свойства существующего объекта
state.count = 1

Также нельзя деструктурировать без потери реактивности:

import { reactive, toRefs } from 'vue'

const state = reactive({ x: 1, y: 2 })

// ПЛОХО — x и y теряют реактивность
const { x, y } = state

// ХОРОШО — используй toRefs
const { x, y } = toRefs(state)
console.log(x.value)  // теперь x — реактивный ref

computed() — вычисляемые свойства

computed() создаёт производное реактивное значение:

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('Алексей')
const lastName = ref('Иванов')

// Автоматически пересчитывается при изменении зависимостей
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

console.log(fullName.value)  // 'Алексей Иванов'
firstName.value = 'Борис'
console.log(fullName.value)  // 'Борис Иванов'
</script>

Примеры

Реализация ref и computed через замыкания — упрощённая модель того, как это работает в Vue

// Vue реализует ref и computed через сложную систему отслеживания зависимостей.
// Покажем упрощённую версию через замыкания.

// Простой ref — хранит значение и уведомляет подписчиков
function ref(initialValue) {
  let _value = initialValue
  const _subscribers = new Set()

  return {
    get value() {
      return _value
    },
    set value(newVal) {
      if (newVal !== _value) {
        _value = newVal
        // Уведомляем всех подписчиков об изменении
        _subscribers.forEach(fn => fn())
      }
    },
    // Подписаться на изменения (упрощённый аналог watch)
    subscribe(fn) {
      _subscribers.add(fn)
      return () => _subscribers.delete(fn)  // возвращаем функцию отписки
    }
  }
}

// computed — вычисляется лазово, кэширует результат
function computed(getter) {
  let _cachedValue
  let _isDirty = true  // нужно ли пересчитать

  return {
    get value() {
      if (_isDirty) {
        _cachedValue = getter()
        _isDirty = false
        console.log('  [computed] пересчитан')
      } else {
        console.log('  [computed] из кэша')
      }
      return _cachedValue
    },
    // Пометить computed как "устаревший" (нужен пересчёт)
    invalidate() {
      _isDirty = true
    }
  }
}

// --- Демонстрация ---

const firstName = ref('Алексей')
const lastName = ref('Иванов')

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// Подписываем computed на изменения ref'ов
const unsubFirst = firstName.subscribe(() => fullName.invalidate())
const unsubLast = lastName.subscribe(() => fullName.invalidate())

console.log('=== Первое чтение (пересчёт) ===')
console.log(fullName.value)  // Алексей Иванов

console.log('\n=== Второе чтение (из кэша) ===')
console.log(fullName.value)  // Алексей Иванов (из кэша)

console.log('\n=== Изменяем firstName ===')
firstName.value = 'Борис'
console.log(fullName.value)  // Борис Иванов (пересчитан)

console.log('\n=== Читаем ещё раз ===')
console.log(fullName.value)  // Борис Иванов (из кэша)

// Чистим подписки
unsubFirst()
unsubLast()