javascript

ReactiveEffect во Vue 3: что на самом деле исполняет вашу реактивность

  • вторник, 17 февраля 2026 г. в 00:00:04
https://habr.com/ru/articles/996880/

В предыдущей статье мы разобрали trackOpBits — механизм оптимизации трекинга зависимостей во Vue 3.
Но тогда мы смотрели на систему через одну конкретную оптимизацию.

Сегодня поднимемся уровнем выше.

Почти всё, что вы делаете во Vue:

  • watchEffect

  • watch

  • computed

  • рендер компонента

— в конечном итоге создаёт экземпляр одного и того же класса.

Этот класс называется ReactiveEffect.

Если разобраться с тем, как он устроен, то реактивность Vue перестанет быть для вас “магией” и станет предсказуемым и понятным графом зависимостей и исполнений.

Разберём:

  • что такое dep

  • как работает track() и trigger()

  • где в этой системе живёт ReactiveEffect

  • как устроен его жизненный цикл

  • зачем ему scheduler

  • и можно ли использовать его напрямую


Слой хранения зависимостей: target → key → dep

Начнём с основы.

Когда вы делаете объект реактивным:

const state = reactive({ count: 0 })

Vue создаёт proxy.
Каждое чтение свойства проходит через getter, каждое изменение — через setter.

Чтобы реактивность работала, Vue должен где-то хранить связи между:

  • свойством

  • и теми, кто от него зависит

Для этого используется структура:

WeakMap<
  target,
  Map<
    key,
    Dep
  >
>

Где:

  • target — исходный объект

  • key — конкретное свойство

  • Dep — структура, хранящая эффекты, подписанные на это свойство

Что такое Dep?

Концептуально — это набор эффектов (раньше можно было представить как Set<ReactiveEffect>).
В текущей реализации это более оптимизированная структура, но по смыслу — список подписчиков.

Важно:
Dep существует для конкретной пары (target, key).

Если два эффекта читают state.count, они оба окажутся в одном и том же dep.


Что делает track()

Теперь ключевой механизм.

Когда внутри эффекта выполняется чтение:

state.count

Vue вызывает внутреннюю функцию:

track(target, key)

Упрощённо она выглядит так:

function track(target, key) {
  if (!activeEffect) return

  const dep = getDep(target, key)

  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

Здесь важно несколько вещей:

  1. track() работает только если есть activeEffect

  2. Эффект добавляется в dep

  3. Сам эффект сохраняет ссылку на dep

Почему эффект хранит dep?
Чтобы управлять своими зависимостями (например, при остановке).

Таким образом:

  • dep знает, какие эффекты зависят от свойства

  • эффект знает, от каких dep он зависит

Получается двусторонняя связь.


Где появляется activeEffect

activeEffect — это глобальная переменная, указывающая на текущий выполняемый эффект.

Она устанавливается внутри метода run() у ReactiveEffect.

Вот упрощённая версия:

let activeEffect

class ReactiveEffect {
  constructor(fn, scheduler?) {
    this.fn = fn
    this.scheduler = scheduler
    this.deps = []
    this.active = true
  }

  run() {
    if (!this.active) {
      return this.fn()
    }

    const parent = activeEffect
    activeEffect = this

    try {
      return this.fn()
    } finally {
      activeEffect = parent
    }
  }
}

Когда вызывается run():

  1. Эффект становится активным

  2. Любой track() внутри fn() регистрирует именно его

  3. После завершения выполнения активный эффект восстанавливается

Именно так строится стек вложенных эффектов.


Что делает trigger()

Теперь вторая часть механизма.

Когда происходит изменение:

state.count++

вызывается:

trigger(target, key)

Он:

  1. Находит dep для (target, key)

  2. Проходит по всем эффектам

  3. Повторно их запускает

Упрощённо:

function trigger(target, key) {
  const dep = getDep(target, key)

  for (const effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

Таким образом:

  • чтение создаёт связь

  • изменение инициирует повторное выполнение

Это и есть реактивность.


ReactiveEffect как единица исполнения

Теперь становится понятно:

ReactiveEffect — это не “просто класс”.

Это:

исполняемая функция, которая автоматически подписывается на всё, что читает.

Он связывает:

  • граф зависимостей (dep)

  • и механизм повторного выполнения (trigger)

Без ReactiveEffect реактивность невозможна.


Где создаются ReactiveEffect в реальном Vue

1. Рендер компонента

Каждый компонент создаёт собственный render-эффект.

При монтировании создаётся:

instance.update = new ReactiveEffect(componentUpdateFn, scheduler)

componentUpdateFn выполняет render и diff.
Когда изменяются реактивные данные, этот эффект запускается повторно — происходит перерендер.


2. computed

computed() создаёт ReactiveEffect, но с особенностями:

  • он lazy (не запускается сразу)

  • хранит кешированное значение

  • использует флаг dirty

  • имеет собственный scheduler

Упрощённая идея:

const effect = new ReactiveEffect(getter, () => {
  if (!dirty) {
    dirty = true
    trigger(...)
  }
})

То есть computed — это тоже эффект, просто с дополнительной логикой.


3. watchEffect

watchEffect — это практически прямое создание ReactiveEffect.


4. watch

watch строится поверх эффекта, но добавляет:

  • сравнение старого и нового значения

  • управление flush

  • обработку cleanup

Но в основе — всё тот же ReactiveEffect.


Scheduler — почему эффект не всегда запускается сразу

ReactiveEffect принимает второй аргумент — scheduler.

Если он передан, при trigger вызывается не run(), а scheduler.

Это позволяет:

  • делать batching обновлений

  • откладывать выполнение

  • управлять очередями

Пример:

const effect = new ReactiveEffect(
  () => {
    console.log(state.count)
  },
  () => {
    queueMicrotask(() => effect.run())
  }
)

Теперь обновления будут происходить асинхронно.

Именно так Vue управляет рендер-очередями.


Можно ли использовать ReactiveEffect напрямую

Да.

И не только внутри Vue.

Пакет @vue/reactivity можно использовать отдельно:

import { reactive, ReactiveEffect } from '@vue/reactivity'

const state = reactive({ value: 1 })

const effect = new ReactiveEffect(() => {
  console.log('value:', state.value)
})

effect.run()
state.value = 2

Это полноценная реактивная система без компонентов.


Что нужно учитывать при ручном использовании

ReactiveEffect — низкоуровневый инструмент.

Важно:

  • вызывать stop() при необходимости

  • понимать, что каждый эффект добавляется в dep

  • учитывать, что большое число эффектов увеличивает стоимость trigger

Это инструмент для продвинутых сценариев:

  • кастомные state-менеджеры

  • интеграция с другими системами

  • тонкая настройка scheduler

В обычном приложении достаточно watch и computed.


Как это связано с trackOpBits

В прошлой статье мы разобрали битовые маски.

Теперь становится ясно, где они работают:

  • внутри run()

  • при регистрации зависимостей

  • при вложенных эффектах

ReactiveEffect — это “контейнер выполнения”.
trackOpBits — механизм оптимизации его поведения при глубокой вложенности.


Итог

Если посмотреть на Vue 3 с архитектурной точки зрения, система выглядит так:

  • Proxy перехватывает чтение и запись

  • track() строит граф зависимостей

  • trigger() инициирует повторное выполнение

  • ReactiveEffect — узел исполнения этого графа

  • scheduler управляет временем выполнения

Всё остальное — обёртки над этим механизмом.

Понимание ReactiveEffect позволяет видеть реактивность Vue как систему, в которой каждое чтение формирует зависимость, а каждое изменение инициирует конкретное повторное выполнение.