ref, reactive, shallowRef, toRaw, markRaw

Урок разбирает, чем ref отличается от reactive, как устроен .value, и зачем нужны поверхностные и исключающие реактивность инструменты shallowRef, toRaw и markRaw.

ref оборачивает любое значение в реактивный объект с полем .value; reactive делает реактивным сам объект через Proxy без обёртки.

В Composition API два главных способа создать реактивное состояние — ref и reactive. На первый взгляд они взаимозаменяемы, и новички выбирают наугад. Но у них разная природа, и от выбора зависит, сохранится ли реактивность при передаче значения дальше. Плюс есть «поверхностные» варианты (shallowRef, shallowReactive) и способы вовсе исключить объект из реактивности (toRaw, markRaw) — они нужны для производительности и интеграции со сторонними библиотеками.

Главная идея, которую стоит усвоить: reactive работает только с объектами и только пока вы держите сам прокси; ref работает с чем угодно (включая примитивы) и переносит реактивность через .value, потому что .value — это геттер/сеттер, который никуда не девается при передаче ref как значения.

Зачем это на практике

Вы пишете composable-функцию useCounter(), которая возвращает счётчик. Если внутри использовать reactive({ count: 0 }) и вернуть его, а вызывающий код напишет const { count } = useCounter() — реактивность счётчика потеряется (примитив скопируется). А вот ref(0) вернётся как объект с .value, и его можно деструктурировать, передавать в функции, класть в массивы — связь сохранится. Поэтому из composables почти всегда возвращают ref (или toRefs от reactive).

ref: реактивность через .value

ref(x) создаёт объект { value: x }, где value — не обычное поле, а пара геттер/сеттер. Геттер вызывает track, сеттер — trigger. Если внутрь положить объект, ref «под капотом» оборачивает его в reactive, чтобы вложенные поля тоже были реактивны (это глубокая реактивность ref по умолчанию).

// Упрощённый ref на чистом JS: .value как геттер/сеттер.
let activeEffect = null;
function effect(fn){ activeEffect = fn; fn(); activeEffect = null; }

function ref(value) {
  const subs = new Set();
  return {
    get value() { if (activeEffect) subs.add(activeEffect); return value; },
    set value(v) { if (v === value) return; value = v; subs.forEach(f => f()); }
  };
}

const count = ref(0);
effect(() => console.log('count.value =', count.value));
count.value = 1;   // изменилось -> эффект перезапустится
count.value = 1;   // то же значение -> эффект НЕ сработает
count.value = 2;

Вывод:

count.value = 0
count.value = 1
count.value = 2

Обратите внимание: повторная запись того же значения (count.value = 1 второй раз) ничего не печатает. Настоящий ref так и делает — сравнивает старое и новое значение через Object.is и не триггерит, если они равны. Это экономит лишние перерисовки.

В шаблоне .value не пишут: Vue автоматически «разворачивает» ref верхнего уровня (auto-unwrap), поэтому в шаблоне просто {{ count }}. Но в JavaScript-коде .value обязателен — забыть его легко, и тогда вы работаете с самим объектом ref, а не с его значением.

reactive: прокси без обёртки

reactive(obj) возвращает Proxy поверх объекта. Доступ к полям — обычный (state.count, без .value), реактивность глубокая: вложенные объекты тоже оборачиваются в прокси при первом доступе. Но есть жёсткие ограничения: работает только с объектами/массивами/коллекциями (примитив reactive(5) бессмыслен), и нельзя «распаковать» поля без потери связи.

Критерийrefreactive
Что оборачиваетлюбое значение (примитив, объект)только объект/массив/Map/Set
Доступ в JSчерез .valueнапрямую
Доступ в шаблонеавто-unwrap, без .valueнапрямую
Переживает деструктуризациюда (ref — объект)нет (поле копируется)
Замена целикомobj.value = newObj — реактивнопереприсваивание переменной рвёт связь

Практическое правило: для отдельных значений и для composables берите ref; reactive удобен для локального сгруппированного состояния внутри одного компонента, которое вы не передаёте наружу.

Поверхностная реактивность: shallowRef и shallowReactive

По умолчанию реактивность глубокая: Vue рекурсивно оборачивает вложенные объекты. Для больших структур это стоит памяти и времени. shallowRef делает реактивной только саму ссылку .value — замена всего значения триггерит эффекты, но мутация вложенных полей не отслеживается. shallowReactive — аналогично: реактивны только свойства верхнего уровня, вложенные объекты остаются «сырыми».

// Имитируем разницу глубокого и поверхностного ref.
let activeEffect = null;
function effect(fn){ activeEffect = fn; fn(); activeEffect = null; }
function shallowRef(value){
  const subs = new Set();
  return {
    get value(){ if(activeEffect) subs.add(activeEffect); return value; },
    set value(v){ value = v; subs.forEach(f => f()); }   // триггерит ТОЛЬКО замена .value
  };
}

const data = shallowRef({ count: 1 });
effect(() => console.log('count =', data.value.count));
data.value.count = 2;          // мутация вложенного поля — НЕ триггерит
console.log('после мутации, эффект не сработал');
data.value = { count: 3 };     // замена value целиком — триггерит

Вывод:

count = 1
после мутации, эффект не сработал
count = 3

Типичный сценарий shallowRef — хранить большой неизменяемый объект (например, инстанс сторонней библиотеки или большой набор данных), который вы всегда заменяете целиком, а не правите по полю. Так Vue не тратит силы на глубокое оборачивание.

Исключение из реактивности: toRaw и markRaw

toRaw(proxy) возвращает исходный «сырой» объект, который прячется за прокси. Полезно, когда нужно сравнить объект по идентичности, отдать его сторонней библиотеке или избежать накладных расходов прокси в горячем цикле. Важно: мутации сырого объекта не триггерят эффекты — это осознанный выход из реактивности.

markRaw(obj) ставит на объект скрытую метку «никогда не делать реактивным». После этого даже если положить его в reactive или ref, Vue не станет его оборачивать. Это нужно для объектов, которые принципиально не должны быть реактивными: инстансы классов сторонних библиотек (карты, графики, редакторы), большие неизменяемые справочники, объекты с круговыми ссылками.

import { reactive, markRaw, toRaw } from 'vue'

// Сторонний объект, который НЕ должен оборачиваться в прокси
const chart = markRaw(new SomeChartLibrary())

const state = reactive({
  chart,              // останется сырым: markRaw победил
  filters: { q: '' }  // обычное реактивное поле
})

// Достать исходный объект из прокси (например, для сравнения по ссылке)
const rawState = toRaw(state)
console.log(rawState === state)   // false: state — это Proxy, rawState — исходный объект

Этот фрагмент использует import из 'vue' и сторонний класс, поэтому он помечен language-text — в браузерной песочнице его запустить нечем, он для чтения.

Как это работает под капотом

Vue хранит реестр соответствий «сырой объект ↔ прокси» в двух WeakMap: один отображает сырой объект в его реактивный прокси, другой — прокси обратно в сырой. Поэтому повторный reactive(obj) от того же объекта возвращает тот же прокси (а не создаёт новый), а toRaw мгновенно находит исходник. markRaw просто ставит на объект неперечисляемое свойство-флаг (__v_skip); функция оборачивания, увидев флаг, возвращает объект как есть.

Авто-unwrap ref в шаблоне и в reactive-объектах — отдельная механика: если положить ref как значение в reactive, доступ к этому полю автоматически разворачивается в .value. Но в массивах и Map такого разворачивания нет — там придётся писать .value явно.

Частые ошибки

Забыть .value в JS-коде. if (count) вместо if (count.value) — частая ошибка: объект ref всегда истинный, условие сработает не так, как ждёте.

Деструктурировать reactive. const { count } = reactive({count:0}) копирует примитив и рвёт связь. Для распаковки используйте toRefs (см. урок про ловушки).

Мутировать вложенное в shallowRef. shallowRef({a:1}).value.a = 2 не вызовет обновления — поверхностный ref следит только за заменой .value целиком.

Ждать реактивности от markRaw-объекта. Помеченный объект никогда не станет реактивным, даже внутри reactive. Если позже он вам понадобится реактивным — метку не снять, нужен другой объект.

Итоги

  • ref оборачивает любое значение и даёт реактивное .value (геттер track, сеттер trigger); переживает деструктуризацию, потому что это объект.
  • reactiveProxy поверх объекта без .value; только для объектов/коллекций, теряет связь при распаковке полей.
  • В шаблоне ref разворачивается автоматически; в JS .value обязателен; в массивах и Map авто-unwrap не работает.
  • shallowRef/shallowReactive делают реактивным только верхний уровень — для больших структур, заменяемых целиком.
  • toRaw достаёт исходный объект из прокси; markRaw навсегда исключает объект из реактивности (флаг __v_skip).
Проверьте себя
1. Почему ref переживает деструктуризацию и передачу в функции, а reactive — нет?
Aref хранит данные в глобальной переменной
Bref — это объект с геттером/сеттером .value, который не теряется при передаче; деструктуризация reactive копирует примитивное значение поля и рвёт связь с прокси
Creactive вообще не реактивен
Dref замораживает значение и его нельзя изменить
2. Что произойдёт, если у shallowRef изменить вложенное поле: shallowRef({count:1}).value.count = 2?
AЭффекты перезапустятся, как при обычном ref
BVue выбросит ошибку
CЭффекты НЕ перезапустятся — shallowRef отслеживает только замену .value целиком
DЗначение не изменится
3. Зачем нужен markRaw?
AЧтобы ускорить глубокое оборачивание объекта в прокси
BЧтобы навсегда пометить объект как нереактивный — даже внутри reactive он не будет обёрнут (полезно для инстансов сторонних библиотек)
CЧтобы превратить ref в reactive
DЧтобы получить .value у обычного объекта