Vue 3 и реактивные модели: Proxy, сигналы, три Echo
У тебя уже два Echo: на React с Redux и на React с Reatom. Сейчас соберём третий,
echo-vue-pinia, на Vue 3. Та же фичу-сетка, тот же backend, но третья модель
реактивности. После этого урока у тебя на руках тройка, на которой видно главное
различие фронтенд-фреймворков: не синтаксис, а то, как они узнают, что
поменялось, и что с этим делают. React сравнивает деревья, сигналы толкают
изменение в подписчиков, Vue смотрит в зеркало через Proxy.
1. Зеркало: Proxy-реактивность
React заставляет тебя описывать изменение явно: вызвал setState, отдал новый
объект, фреймворк сравнил старое дерево с новым. Vue устроен как зеркало. Ты
берёшь обычный с виду объект и меняешь его напрямую, state.count++, а
Proxy
под капотом ловит это чтение и запись. На чтении он запоминает, кто прочитал
свойство, на записи будит ровно тех, кто его читал. Никакого ручного описания
изменений, как в React, и при этом точечно, как в Reatom.
Это и есть третья модель. В уроках 10 до 12 ты видел две: immutable-diff (React) и сигналы (Reatom). Vue добавляет Proxy. К концу урока сравним все три на одной кнопке.
Proxy это не реактивность
На собеседованиях и в спорах часто звучит фраза: «React не реактивный, во Vue реактивность через Proxy». В ней перепутаны дверь и дом. Proxy это только способ перехватить доступ, классический паттерн Proxy из учебников по проектированию: посредник, который вызывается вместо настоящего объекта. Сама система реактивности это другое: структура данных, в которой хранится граф «значение и его читатели», и алгоритм, который по этому графу проходит при изменении. Структура и обход у каждой библиотеки свои.
Два факта, которые легко проверить:
- Vue 2 строил ту же реактивность вообще без Proxy, на
Object.defineProperty: геттер и сеттер на каждое свойство. Модель «чтение собирает зависимость, запись будит читателей» не поменялась, поменялся только механизм перехвата (а заодно ушли ограничения Vue 2 вроде невидимых новых ключей объекта). - Preact Signals не использует Proxy вовсе: чтение перехватывает обычный геттер
.value, а зависимости хранятся в двусвязном списке. Загляни в исходники ядра: типNodeэто узел списка «сигнал и его подписчик», а методaddDependencyподвешивает новую ноду в список при каждом чтении сигнала внутри эффекта.
Так что когда тебя спросят, как работает реактивность во Vue, ответ «через Proxy»
описывает только входную дверь. Настоящий ответ про то, что за дверью: кто кого
читал, как помечаются грязные узлы, в каком порядке перезапускаются эффекты.
Proxy во Vue, геттер .value в Preact Signals, компилятор в Svelte 5 это разные
двери в очень похожие дома.
Что взять с собой
- Vue это зеркало: меняешь reactive-объект напрямую, слежение делает Proxy.
- На чтении Proxy собирает зависимость, на записи будит тех, кто читал свойство.
- Proxy это лишь механизм перехвата (паттерн-посредник), а не сама реактивность.
Vue 2 делал то же на
defineProperty, Preact Signals хранит граф в двусвязном списке без всякого Proxy. - Три модели курса: React (immutable diff), Reatom (signals), Vue (Proxy).
2. ref и reactive
В Vue два способа сделать значение реактивным, и выбор зависит от того, примитив
это или объект.
ref
кладёт значение в контейнер с полем .value и годится для чего угодно, особенно
для примитивов:
import { ref } from 'vue';
const count = ref(0);
count.value; // прочитать: 0
count.value++; // записать
// в шаблоне просто {{ count }}, Vue развернёт .value сам
reactive
оборачивает объект в Proxy, и тогда .value не нужен, доступ прямой:
import { reactive } from 'vue';
const state = reactive({ count: 0, theme: 'light' });
state.count++; // напрямую, Proxy всё видит
Почему два API, а не одно? Примитив нельзя обернуть Proxy, Proxy работает только
с объектами. Поэтому ref кладёт значение в объект-контейнер, а reactive
оборачивает уже готовый объект. На практике многие берут ref везде ради
единообразия и потому, что reactive теряет реактивность при деструктуризации.
Что взять с собой
ref(value)оборачивает что угодно в.value, нужен для примитивов.reactive(obj)оборачивает объект в Proxy, доступ без.value.- Два API потому, что Proxy не умеет примитивы. Чаще берут
refповсеместно. reactiveнельзя деструктурировать: связь с Proxy потеряется.
3. computed и watch
Производное значение в Vue это computed, и работает он как сигнальный мемо:
читает реактивные источники, кэширует результат, пересчитывается только когда
источник изменился. Зависимости отслеживаются сами, списка как в useMemo нет.
import { computed, ref } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2); // кэш, пересчёт при смене count
Побочный эффект на изменение это watch (следит за конкретным источником) или
watchEffect (сам отслеживает, что прочитал):
import { watch, watchEffect } from 'vue';
watch(count, (next, previous) => {
console.log(`было ${previous}, стало ${next}`);
});
watchEffect(() => {
console.log(`count сейчас ${count.value}`); // перезапустится при смене count
});
Сравни с React, и разница в характере. computed это useMemo, но без массива
зависимостей и без риска его забыть: Vue знает зависимости из самого тела.
watch это useEffect, но с точным источником вместо [deps]. Та же боль с
устаревшими зависимостями, которую мы разбирали в уроке 06,
в Vue просто не возникает.
Что взять с собой
computed(() => ...)это кэшированное производное, зависимости отслеживаются сами.watch(source, cb)для эффекта на конкретный источник,watchEffectсам трекает.- Против React: нет массива зависимостей, нет класса багов с устаревшими deps.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.