Интервью-вопрос
Как построена реактивность во Vue3
Vue3 строит реактивность вокруг Proxy, ref и системы зависимостей. В ответе важно показать не только API, но и практические риски: потерю реактивности, лишние эффекты и неверный выбор между ref, reactive, computed и watch.
- Добавлен
- Редакция
Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.
Мини-квиз
Проверка перед разбором
Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.
Вопрос 1 из 60 правильно
Разбор
Разобраться, а не зазубрить
Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.
Базовая идея
Реактивность во Vue3 удобно объяснять через две операции: чтение и запись. При чтении Vue понимает, что текущий рендер, computed, watchEffect или другой effect зависит от конкретного значения. При записи Vue находит зависимые эффекты и запускает обновление.
Поэтому сильный короткий ответ не должен сводиться к фразе, что Vue использует Proxy. Proxy перехватывает операции с объектом. Но обновление UI появляется из связки track, trigger и очереди обновлений.
import { reactive, watchEffect } from "vue";
const state = reactive({ count: 0 });
watchEffect(() => {
console.log(state.count); // чтение, Vue запоминает зависимость
});
state.count += 1; // запись, Vue запускает связанный effectВ компоненте за обновление шаблона отвечает render effect. Он читает реактивные данные во время рендера. Потом Vue обновляет только то, что зависит от измененных данных.
Proxy, ref и dependency tracking
reactive принимает объект и возвращает Proxy. Proxy перехватывает чтение, запись, удаление свойств, проверку наличия ключа и часть операций с массивами и коллекциями. Это улучшило ситуацию по сравнению с подходом на Object.defineProperty, потому что Vue3 лучше видит добавление и удаление свойств.
ref нужен, когда значение не является объектом или когда удобнее иметь одну реактивную ссылку. Внутри это объект с реактивным свойством value. В шаблоне Vue часто разворачивает ref автоматически. В JavaScript-коде нужно явно обращаться к .value.
<script setup>
import { computed, reactive, ref } from "vue";
const count = ref(0);
const user = reactive({ firstName: "Ada", lastName: "Lovelace" });
const fullName = computed(() => `${user.firstName} ${user.lastName}`);
function increment() {
count.value += 1;
}
</script>
<template>
<button @click="increment">{{ count }}</button>
<p>{{ fullName }}</p>
</template>Практический вывод для интервью простой. Называйте не только инструменты, но и их границы. reactive удобен для объекта состояния. ref удобен для отдельных значений. computed хранит производное значение. watch нужен для побочных эффектов.
- 1Компонент рендерится или запускается effect.
- 2Код читает reactive-свойство или ref.value.
- 3Proxy getter или getter ref вызывает track.
- 4Vue связывает текущий effect с прочитанным ключом.
- 1Код записывает новое значение в reactive-свойство или ref.value.
- 2Proxy setter или setter ref вызывает trigger.
- 3Vue находит эффекты, которые зависят от этого ключа.
- 4Обновления попадают в очередь, DOM обновляется батчем.
Как выбирать API в коде
Во Vue3 одну задачу часто можно решить несколькими способами. На интервью важно показать критерий выбора, а не спорить, что один API всегда лучше другого. Ошибка в выборе редко сразу ломает приложение. Чаще она приводит к тонким багам: лишним watcher, рассинхронизации состояния или потере реактивной связи.
Как выбрать API реактивности
Используйте ref и меняйте значение через .value в JavaScript-коде.Используйте reactive, если вам удобно обращаться к полям как state.name и state.email.Используйте toRef или toRefs, а не обычную деструктуризацию.Используйте computed. Так вы не дублируете состояние и получаете кеширование.Используйте watch или watchEffect, потому что это побочный эффект. Для async-кода сразу продумайте отмену, обработку ошибок и состояние loading.Главные ловушки
Первая ловушка, это деструктуризация. Если взять свойство из reactive как обычную переменную, вы можете получить снимок значения вместо реактивной связи.
import { reactive, toRefs } from "vue";
const state = reactive({ count: 0 });
// Плохо: count больше не читает значение через state Proxy.
const { count } = state;
// Лучше: countRef сохраняет связь с state.count.
const { count: countRef } = toRefs(state);
countRef.value += 1;На интервью можно сказать коротко. Proxy отслеживает операции через Proxy. Если я вынес значение в обычную переменную, Vue уже не всегда видит чтение и запись как зависимость.
Вторая ловушка, это raw-объект и идентичность. reactive возвращает Proxy, поэтому исходный объект и реактивный объект не равны по ссылке.
const rawUser = { name: "Ada" };
const user = reactive(rawUser);
console.log(rawUser === user); // false
// Плохо: запись идет не через Proxy, эффекты могут не обновиться.
rawUser.name = "Grace";
// Хорошо: запись идет через Proxy.
user.name = "Grace";Это важно для кэшей, Map, Set, сравнения по ссылке и интеграций с библиотеками. Если нужно исключить объект из реактивности, используют markRaw. Если нужно получить исходный объект, есть toRaw. Но это не обычный путь для бизнес-логики компонента.
Практический вывод для frontend-кода
Хороший ответ заканчивается тем, как это влияет на код компонента. Если данные нужны для UI, меняйте их через реактивную ссылку. Если значение можно вычислить из других значений, не создавайте отдельное состояние. Используйте computed. Если нужно сходить в API, записать в localStorage или отправить аналитику при изменении, используйте watch или watchEffect и следите за cleanup.
import { ref, watch } from "vue";
const query = ref("");
const results = ref([]);
const loading = ref(false);
const error = ref(null);
watch(query, async (value, _previousValue, onCleanup) => {
const trimmed = value.trim();
const controller = new AbortController();
onCleanup(() => controller.abort());
if (!trimmed) {
results.value = [];
error.value = null;
loading.value = false;
return;
}
loading.value = true;
error.value = null;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(trimmed)}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error("Search failed");
}
results.value = await response.json();
} catch (err) {
if (err?.name === "AbortError") {
return;
}
results.value = [];
error.value = "Не удалось загрузить результаты";
} finally {
if (!controller.signal.aborted) {
loading.value = false;
}
}
});Здесь cleanup важен. Без отмены старого запроса можно получить race condition. Пользователь уже ввел новый запрос, а в UI прилетели результаты для старого значения. Без loading и error интерфейс зависнет в непонятном состоянии. Человек не поймет, идет загрузка, результатов нет или запрос сломался.
Что сказать на интервью
Можно ответить так:
Во Vue3 реактивность построена на Proxy для объектов и ref для отдельных значений. Когда код читает реактивное свойство, Vue собирает зависимость. Когда свойство меняется, Vue вызывает связанные эффекты и планирует обновление компонента. Поэтому UI обновляется не потому, что Vue постоянно проверяет все данные, а потому что он знает, какие эффекты зависят от каких ключей.
Если хотите усилить ответ, добавьте практическую часть:
В коде я учитываю несколько ловушек. Не деструктурирую reactive без toRefs, не меняю raw-объект напрямую, использую computed для производных значений, а watch оставляю для побочных эффектов вроде запросов и синхронизации.
Частые ошибки
Где обычно ошибаются
Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.
- 1
Говорить только про Proxy
Proxy важен, но это не вся система. Если не сказать про
track,trigger, эффекты иref, ответ звучит поверхностно. В реальном коде из-за этого легко ошибиться сcomputed,watchи лишними обновлениями. - 2
Деструктурировать reactive как обычный объект
Обычная деструктуризация забирает текущее значение и может обойти Proxy. Из-за этого UI не обновится там, где вы ждете реактивную связь. Безопаснее читать поле через объект или использовать
toRefиtoRefs. - 3
Забывать про .value в JavaScript
В шаблоне Vue часто разворачивает
refавтоматически. Но в кодеsetup, функциях и composable обычно нужно работать с.value. Если забыть это, вы будете менять не значение или получите странное сравнение. На интервью сразу разделяйте поведение в шаблоне и в JavaScript. - 4
Мутировать raw-объект вместо Proxy
Если сохранить исходный объект и менять его напрямую после
reactive, Vue не увидит запись через Proxy. Данные могут измениться, но зависимые эффекты не получат сигнал. Работайте с объектом, который вернулreactive, или явно объясняйте, зачем используетеtoRaw. - 5
Использовать watch вместо computed для производных данных
watchнужен для побочных эффектов, например запроса или записи в storage. Если через него просто пересчитывать значение, появляется лишнее состояние и риск рассинхронизации. Для значения, которое выводится из других данных, лучше выбратьcomputed. - 6
Делать запрос в watch без cleanup
Если запускать
fetchпри каждом изменении поля и не отменять старый запрос, UI может показать устаревшие результаты. Еще хуже, компонент уже ушел со страницы, а старый запрос продолжает менять состояние. Безопаснее использоватьAbortController, обрабатыватьloadingиerror, а cleanup привязывать к watcher.
Follow-up
Что могут спросить дальше
Короткие ответы на вопросы, которыми проверяют понимание реактивности Vue3 и практических ограничений API.
Живые ответы
Видео с похожим вопросом
Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.
Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.
Как навязывается реактивность во Vue 3 😎
Во Vue 3 реактивность появляется, когда данные проходят через ref, reactive и связанные API. Разбираем, как Vue отслеживает чтение и запись, где нужна .value и почему деструктуризация может сломать обновление UI.
Какие библиотеки часто используешь 😎
В ответе важно назвать не просто список библиотек, а задачи, ради которых вы их выбираете, и ограничения этих решений. Разбираем, как показать опыт без хаотичного перечисления пакетов.