ReactiveEffect во Vue 3: что на самом деле исполняет вашу реактивность
- вторник, 17 февраля 2026 г. в 00:00:04
В предыдущей статье мы разобрали trackOpBits — механизм оптимизации трекинга зависимостей во Vue 3.
Но тогда мы смотрели на систему через одну конкретную оптимизацию.
Сегодня поднимемся уровнем выше.
Почти всё, что вы делаете во Vue:
watchEffect
watch
computed
рендер компонента
— в конечном итоге создаёт экземпляр одного и того же класса.
Этот класс называется ReactiveEffect.
Если разобраться с тем, как он устроен, то реактивность Vue перестанет быть для вас “магией” и станет предсказуемым и понятным графом зависимостей и исполнений.
Разберём:
что такое dep
как работает track() и trigger()
где в этой системе живёт ReactiveEffect
как устроен его жизненный цикл
зачем ему scheduler
и можно ли использовать его напрямую
Начнём с основы.
Когда вы делаете объект реактивным:
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.
Теперь ключевой механизм.
Когда внутри эффекта выполняется чтение:
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) } }
Здесь важно несколько вещей:
track() работает только если есть activeEffect
Эффект добавляется в dep
Сам эффект сохраняет ссылку на dep
Почему эффект хранит dep?
Чтобы управлять своими зависимостями (например, при остановке).
Таким образом:
dep знает, какие эффекты зависят от свойства
эффект знает, от каких dep он зависит
Получается двусторонняя связь.
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():
Эффект становится активным
Любой track() внутри fn() регистрирует именно его
После завершения выполнения активный эффект восстанавливается
Именно так строится стек вложенных эффектов.
Теперь вторая часть механизма.
Когда происходит изменение:
state.count++
вызывается:
trigger(target, key)
Он:
Находит dep для (target, key)
Проходит по всем эффектам
Повторно их запускает
Упрощённо:
function trigger(target, key) { const dep = getDep(target, key) for (const effect of dep) { if (effect.scheduler) { effect.scheduler() } else { effect.run() } } }
Таким образом:
чтение создаёт связь
изменение инициирует повторное выполнение
Это и есть реактивность.
Теперь становится понятно:
ReactiveEffect — это не “просто класс”.
Это:
исполняемая функция, которая автоматически подписывается на всё, что читает.
Он связывает:
граф зависимостей (dep)
и механизм повторного выполнения (trigger)
Без ReactiveEffect реактивность невозможна.
Каждый компонент создаёт собственный render-эффект.
При монтировании создаётся:
instance.update = new ReactiveEffect(componentUpdateFn, scheduler)
componentUpdateFn выполняет render и diff.
Когда изменяются реактивные данные, этот эффект запускается повторно — происходит перерендер.
computed() создаёт ReactiveEffect, но с особенностями:
он lazy (не запускается сразу)
хранит кешированное значение
использует флаг dirty
имеет собственный scheduler
Упрощённая идея:
const effect = new ReactiveEffect(getter, () => { if (!dirty) { dirty = true trigger(...) } })
То есть computed — это тоже эффект, просто с дополнительной логикой.
watchEffect — это практически прямое создание ReactiveEffect.
watch строится поверх эффекта, но добавляет:
сравнение старого и нового значения
управление flush
обработку cleanup
Но в основе — всё тот же ReactiveEffect.
ReactiveEffect принимает второй аргумент — scheduler.
Если он передан, при trigger вызывается не run(), а scheduler.
Это позволяет:
делать batching обновлений
откладывать выполнение
управлять очередями
Пример:
const effect = new ReactiveEffect( () => { console.log(state.count) }, () => { queueMicrotask(() => effect.run()) } )
Теперь обновления будут происходить асинхронно.
Именно так Vue управляет рендер-очередями.
Да.
И не только внутри 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.
В прошлой статье мы разобрали битовые маски.
Теперь становится ясно, где они работают:
внутри run()
при регистрации зависимостей
при вложенных эффектах
ReactiveEffect — это “контейнер выполнения”.
trackOpBits — механизм оптимизации его поведения при глубокой вложенности.
Если посмотреть на Vue 3 с архитектурной точки зрения, система выглядит так:
Proxy перехватывает чтение и запись
track() строит граф зависимостей
trigger() инициирует повторное выполнение
ReactiveEffect — узел исполнения этого графа
scheduler управляет временем выполнения
Всё остальное — обёртки над этим механизмом.
Понимание ReactiveEffect позволяет видеть реактивность Vue как систему, в которой каждое чтение формирует зависимость, а каждое изменение инициирует конкретное повторное выполнение.