Как устроена реактивность внутри: Proxy, track, trigger, effect

↑ 7.1 Vue под капотом · → Следующая

Желательно~4 мин чтенияУровень: senior

Зачем это на собесе

Классический вопрос middle+: «как Vue узнаёт, что данные изменились». Нужно уверенно назвать Proxy, track/trigger и effect.

Идея

Vue 3 оборачивает объект в Proxy. При чтении свойства он запоминает, какой эффект (effect) сейчас выполняется (track). При записи — находит подписанные эффекты и запускает их (trigger).

Мини-реализация

let activeEffect: (() => void) | null = null
const targetMap = new WeakMap<object, Map<PropertyKey, Set<() => void>>>()
 
function track(target: object, key: PropertyKey) {
  if (!activeEffect) return
  let deps = targetMap.get(target)
  if (!deps) targetMap.set(target, (deps = new Map()))
  let dep = deps.get(key)
  if (!dep) deps.set(key, (dep = new Set()))
  dep.add(activeEffect)
}
 
function trigger(target: object, key: PropertyKey) {
  targetMap.get(target)?.get(key)?.forEach(fn => fn())
}
 
function reactive<T extends object>(obj: T): T {
  return new Proxy(obj, {
    get(t, k, r) { track(t, k); return Reflect.get(t, k, r) },
    set(t, k, v, r) { const ok = Reflect.set(t, k, v, r); trigger(t, k); return ok },
  })
}
 
function effect(fn: () => void) {
  activeEffect = fn
  fn()
  activeEffect = null
}

Структура

ПонятиеРоль
reactiveProxy над объектом (глубокий)
refконтейнер { value } с геттером и сеттером, для примитивов
computedленивый эффект с кэшем, инвалидируется при смене зависимостей
watchEffect, renderэффекты, подписанные на реактивные данные

Vue 2 vs Vue 3

Vue 2 использовал Object.defineProperty: не видел добавление и удаление свойств, индексы массивов (Vue.set). Proxy перехватывает всё: добавление, удаление, in, итерацию, Map и Set.

Нюансы

  • деструктуризация reactive теряет реактивность — используйте toRefs;
  • ref в шаблоне разворачивается автоматически, в reactive тоже (unwrap);
  • Proxy создаётся лениво при первом обращении к вложенному объекту;
  • начиная с Vue 3.5 реактивная система переписана: двусвязный список зависимостей и версии счётчиков, потребление памяти на ~56% меньше.

Вопросы с ответами