Vue.jsSeniorTechnical

Как работает система реактивности 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 глубокой реактивности.

Sources

Related topics