Как работает система реактивности Vue? Что изменилось между Vue 2 и Vue 3 (Proxy вместо Object.defineProperty)?
Vue 2 использовал Object.defineProperty, что не позволяло реагировать на добавление свойств и индексный доступ к массивам. Vue 3 перешёл на Proxy, который перехватывает все операции и устраняет эти ограничения. Внутренне это реализовано через track/trigger и ReactiveEffect.
Реактивность Vue 2: Object.defineProperty
В Vue 2 реактивность реализована через Object.defineProperty. При инициализации компонента Vue рекурсивно обходит все свойства объекта data и заменяет каждое геттером и сеттером. Когда вычитывается свойство во время рендера, текущий watcher добавляется в список подписчиков (dep). При записи — dep уведомляет все watchers.
// Упрощённая схема Vue 2
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) dep.depend(); // подписать watcher
return val;
},
set(newVal) {
val = newVal;
dep.notify(); // уведомить всех
}
});
}
Ограничения Vue 2
- Добавление новых свойств — не реактивно.
this.obj.newProp = 1не вызовет ре-рендер. НужноVue.set(this.obj, 'newProp', 1)илиthis.$set. - Удаление свойств — аналогично, нужен
Vue.delete. - Индексы массива —
this.arr[2] = 'x'не реактивно. НужноVue.set(this.arr, 2, 'x')или методы-мутаторы (push,splice, etc.) — Vue патчил их. - Производительность — при старте глубокий обход дерева объектов через defineProperty был дорогим для крупных объектов.
Реактивность Vue 3: Proxy
Vue 3 использует нативный Proxy, перехватывающий все операции над объектом — чтение (get), запись (set), удаление (deleteProperty), проверку (has), итерацию (ownKeys). Это устраняет все ограничения Vue 2.
import { reactive, effect } from '@vue/reactivity';
const state = reactive({ count: 0, items: [] });
effect(() => {
// автоматически подписывается на все прочитанные свойства
console.log('count:', state.count);
});
state.count++; // effect запустится снова
state.newProp = 'x'; // тоже реактивно — Proxy перехватит set
delete state.count; // deleteProperty — тоже отслеживается
state.items[0] = 'a'; // индексный доступ — работает
state.items.length = 0; // изменение length — работает
Внутренний механизм Vue 3
Система реактивности состоит из трёх слоёв:
- reactive() / ref() — создают реактивные прокси.
refоборачивает примитивы в объект с.value. - track() / trigger() — функции из
@vue/reactivity.trackвызывается в геттере Proxy и записывает зависимость текущего active effect.triggerвызывается в сеттере и запускает все зависимые effects. - effect() / ReactiveEffect — базовый примитив (используется внутри
computed,watch, рендер-функции компонента).
// Как устроен reactive() упрощённо
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key); // записать зависимость
return Reflect.get(obj, key, receiver);
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver);
trigger(obj, key); // уведомить effects
return result;
},
deleteProperty(obj, key) {
const result = Reflect.deleteProperty(obj, key);
trigger(obj, key);
return result;
}
});
}
ref vs reactive
ref(value)— для примитивов и любых значений. Доступ через.valueв JS, авто-unwrap в шаблоне.reactive(obj)— для объектов. Деструктуризация разрушает реактивность — нуженtoRefs().
Подводные камни
- Proxy не работает в IE11 — если нужна поддержка IE, Vue 2 или полифилл обязательны.
- Деструктуризация реактивного объекта:
const { count } = reactive({ count: 0 })—countтеряет реактивность. Всегда используйтеtoRefs()или оставляйте обращение черезstate.count. - Вложенные объекты в
reactive()становятся реактивными лениво через Proxy, но сам объект не клонируется — мутация исходного объекта влияет на реактивное состояние. - Замена всего объекта:
state = reactive(newObj)разрывает реактивную связь. НужноObject.assign(state, newObj)илиstore.$patch(newObj)в Pinia. refвreactive-объекте авто-распаковывается:const s = reactive({ n: ref(0) }); s.n++работает без.value— это удобно, но может запутать.- Шаллоу-версии (
shallowRef,shallowReactive) не отслеживают вложенные свойства — используйте их только осознанно для оптимизации больших объектов. markRaw(obj)исключает объект из системы реактивности навсегда. Передача такого объекта вreactiveприведёт к предупреждению в DEV-режиме.
Common mistakes
- Считать, что
Object.definePropertyвсё ещё используется в Vue 3. - Мутировать сырой объект вместо реактивной обёртки и удивляться, что UI не обновился.
- Деструктурировать
reactiveи терять реактивность безtoRefs. - Полагаться на
Vue.set— его в Vue 3 нет.
What the interviewer is testing
- Знает, что в основе Vue 3 лежит
Proxyс паройtrack/trigger. - Может описать роль planner/
nextTickи пакетной обработки. - Понимает, почему добавление нового ключа работает в Vue 3 и не работало в Vue 2.
- Упоминает
shallowRef/markRawкак способ снять overhead глубокой реактивности.