Реактивность под капотом и ловушка деструктуризации

Заглядываем под капот реактивности и разбираем ошибку №1 новичков — деструктуризацию reactive-объекта, после которой интерфейс перестаёт обновляться.

Реактивность во Vue 3 построена на Proxy: Vue оборачивает объект и перехватывает чтение и запись свойств, чтобы знать, где значение используется, и что обновлять при изменении.

Как Vue «узнаёт» об изменениях

Когда вы создаёте reactive({ count: 0 }), Vue возвращает не сам объект, а Proxy вокруг него. Любое чтение свойства Vue запоминает («этот кусок шаблона зависит от count»), а любая запись — запускает обновление зависимых мест. Упрощённая модель такого перехвата:

function makeReactive(obj, onChange) {
  return new Proxy(obj, {
    set(target, key, value) {
      target[key] = value;
      onChange(key, value);     // Vue здесь запускает перерисовку
      return true;
    },
  });
}

const state = makeReactive({ count: 0 }, (k, v) =>
  console.log(`Свойство ${k} стало ${v}`)
);

state.count = 1;
state.count = 2;
console.log("Итог:", state.count);

Вывод:

Свойство count стало 1
Свойство count стало 2
Итог: 2

Ключевая мысль: реактивность живёт в самом прокси-объекте. Перехват срабатывает, только когда вы обращаетесь к свойству через этот объект (state.count).

Ловушка: деструктуризация reactive

Теперь главная ошибка. Деструктуризация вынимает значение свойства в обычную переменную — и связь с прокси теряется. Обновление такой переменной Vue уже не видит:

function makeReactive(obj, onChange) {
  return new Proxy(obj, {
    set(t, k, v) { t[k] = v; onChange(k, v); return true; },
  });
}
const state = makeReactive({ count: 0 }, () => console.log("перерисовка!"));

// Деструктуризация: count — теперь просто число 0, не связано с прокси
let { count } = state;
count = 5;                 // прокси не знает об этом — нет "перерисовка!"
console.log("Локальная count:", count);
console.log("В состоянии:", state.count);   // там по-прежнему 0

Вывод:

Локальная count: 5
В состоянии: 0

Видите: мы поменяли локальную переменную, но прокси не сработал, и реальное состояние осталось прежним. В реальном Vue это выглядит так: «я меняю переменную, а на экране ничего не происходит». Это и есть классическая боль новичков с reactive.

Как делать правильно

ПодходРеактивность
Обращаться через объект: state.countработает
Деструктуризация: const { count } = stateтеряется
toRefs(state) перед деструктуризациейсохраняется
Использовать ref вместо reactiveпроблемы нет

Если нужно «разобрать» reactive-объект на переменные, оберните его в toRefs — он превратит каждое свойство в отдельный ref, сохранив связь:

<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Аня' })
const { count, name } = toRefs(state)   // count и name — реактивные ref
</script>

Почему ref проще

С ref этой ловушки нет: значение лежит в .value, и вы всегда работаете с обёрткой целиком. Это ещё один аргумент в пользу «ref по умолчанию» из прошлого урока.

Итог

  • Реактивность Vue 3 построена на Proxy: перехват чтения/записи свойств объекта.
  • Реактивность живёт в прокси-объекте; обращаться нужно через него (state.count).
  • Деструктуризация reactive вынимает значение и рвёт связь — интерфейс перестаёт обновляться.
  • Решения: toRefs перед деструктуризацией или использование ref.
Проверьте себя
1. На каком механизме JavaScript построена реактивность Vue 3?
AНа setInterval, который опрашивает данные
BНа Proxy, который перехватывает чтение и запись свойств объекта
CНа localStorage
DНа прямом редактировании DOM
2. Что происходит при деструктуризации reactive-объекта, например const { count } = state?
Acount остаётся полностью реактивным
Bcount становится обычным значением и теряет связь с прокси — обновления не отслеживаются
CVue выдаёт ошибку и не запускается
Dcount превращается в ref автоматически
3. Как безопасно деструктурировать reactive-объект, сохранив реактивность?
AИспользовать JSON.parse(JSON.stringify(state))
BОбернуть объект в toRefs(state) перед деструктуризацией
CДобавить .value к каждому свойству
DДеструктурировать внутри template
Поддержать проект