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) бессмыслен), и нельзя «распаковать» поля без потери связи.
| Критерий | ref | reactive |
| Что оборачивает | любое значение (примитив, объект) | только объект/массив/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); переживает деструктуризацию, потому что это объект.reactive—Proxyповерх объекта без.value; только для объектов/коллекций, теряет связь при распаковке полей.- В шаблоне ref разворачивается автоматически; в JS
.valueобязателен; в массивах и Map авто-unwrap не работает. shallowRef/shallowReactiveделают реактивным только верхний уровень — для больших структур, заменяемых целиком.toRawдостаёт исходный объект из прокси;markRawнавсегда исключает объект из реактивности (флаг__v_skip).