Реактивность
Вопрос
Как Vue 3 узнаёт, что шаблон надо обновить, и когда реактивность молча не срабатывает?
Короткий ответ
reactive оборачивает объект в Proxy. Чтение свойства внутри эффекта (рендер, computed, watch) подписывает эффект на это свойство. Запись уведомляет подписчиков.
ref кладёт значение в объект с полем .value и делает реактивным его. В шаблоне ref разворачивается, .value писать не нужно. В скрипте нужно.
Во Vue 2 геттеры и сеттеры вешались на поля, которые уже были в data. Добавление ключа и замена элемента массива по индексу требовали Vue.set. Во Vue 3 Proxy видит и новые ключи, и операции с массивом.
Не отслеживаются:
- деструктуризация реактивного объекта без
toRefs— в локальные переменные попадают сырые значения; - замена реактивного объекта обычным, если вы потеряли ссылку на прокси;
- поля
MapиSet, если пользоваться ими не через реактивную обёртку Vue, которая перехватывает их методы.
Разбор
const state = reactive({ count: 0 })
const { count } = state // count больше не реактивен
const { count: countRef } = toRefs(state) // countRef.value связан со stateref берут и для объектов, если нужна одна привычка: значение всегда за .value, ссылку можно заменить целиком. reactive удобен для группы полей формы. Его нельзя переприсвоить (state = other рвёт связь с прокси) и нельзя деструктурировать без toRefs.
toRefs(object) делает ref на каждое поле, деструктуризация связь не теряет. toRef(object, 'key') — ref на одно поле, даже если ключа ещё нет. toValue() (Vue 3.3+) достаёт сырое значение из примитива, ref или геттера. Это вход гибкого composable.
computed пересчитывается, когда читают его значение и грязные зависимости. Не читаемый computed работу не делает. watch по умолчанию ленивый и даёт новое и старое значение. watchEffect бежит сразу и собирает зависимости сам, старого значения у него нет. customRef нужен, когда момент track / trigger надо задать руками, например debounce поля ввода.
Глубокий watch(() => state, cb, { deep: true }) дорогой на больших деревьях. Лучше следить за конкретным полем или за геттером, который возвращает нужный срез.
Проп нельзя мутировать: изменение поля пропа во Vue 3 в режиме разработки предупреждает. Данные поднимают событием наверх или копируют в локальный ref, если это черновик формы.
На что смотрят на senior
Реактивное дерево, которое передают в большую таблицу целиком, будит лишние эффекты. Узкий computed для видимых строк и стабильные ключи списка важнее ручного «кеша». shallowRef уместен для неизменяемого снимка с сервера, который заменяют целиком: глубокая проксификация большого JSON ничего не даёт, если поля по отдельности не меняют.
markRaw помечает объект, который нельзя оборачивать: экземпляр карты, класс из сторонней библиотеки. Иначе Vue сделает из него прокси и сломает идентичность.
Типичные ошибки
- Забыть
.valueу ref в скрипте и записать новое число мимо реактивности. - Деструктурировать
propsв setup и удивляться, что значения не обновляются. - Вешать глубокий watch на весь стор.
- Ждать
Vue.setи писать его в коде Vue 3.
Задача
Шаблон не обновляется. Где пропадает реактивность?
const state = reactive({ count: 0 })
const { count } = state
function inc() {
count += 1
}Показать ответ
Деструктуризация снимает число с прокси. count — обычная переменная, count += 1 не пишет в state.count.
Нужно менять state.count или взять const { count } = toRefs(state) и писать count.value. Полная замена state = { count: 1 } тоже оторвёт шаблон: прокси останется старым.