- Регистрация
- 23 Авг 2023
- Сообщения
- 3,969
- Реакции
- 0
- Баллы
- 36
Ofline
В предыдущей статье мы разобрали trackOpBits — механизм оптимизации трекинга зависимостей во Vue 3.
Но тогда мы смотрели на систему через одну конкретную оптимизацию.
Сегодня поднимемся уровнем выше.
Почти всё, что вы делаете во Vue:
— в конечном итоге создаёт экземпляр одного и того же класса.
Этот класс называется ReactiveEffect.
Если понимать, как он устроен, реактивность Vue перестаёт быть “магией” и становится предсказуемым графом зависимостей и исполнений.
Разберём:
Начнём с основы.
Когда вы делаете объект реактивным:
const state = reactive({ count: 0 })
Vue создаёт proxy.
Каждое чтение свойства проходит через getter, каждое изменение — через setter.
Чтобы реактивность работала, Vue должен где-то хранить связи между:
Для этого используется структура:
WeakMap<
target,
Map<
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)
}
}
Здесь важно несколько вещей:
Почему эффект хранит 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():
Именно так строится стек вложенных эффектов.
Что делает trigger()
Теперь вторая часть механизма.
Когда происходит изменение:
state.count++
вызывается:
trigger(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 как единица исполнения
Теперь становится понятно:
ReactiveEffect — это не “просто класс”.
Это:
исполняемая функция, которая автоматически подписывается на всё, что читает.
Он связывает:
Без ReactiveEffect реактивность невозможна.
Где создаются ReactiveEffect в реальном Vue
1. Рендер компонента
Каждый компонент создаёт собственный render-эффект.
При монтировании создаётся:
instance.update = new ReactiveEffect(componentUpdateFn, scheduler)
componentUpdateFn выполняет render и diff.
Когда изменяются реактивные данные, этот эффект запускается повторно — происходит перерендер.
2. computed
computed() создаёт ReactiveEffect, но с особенностями:
Упрощённая идея:
const effect = new ReactiveEffect(getter, () => {
if (!dirty) {
dirty = true
trigger(...)
}
})
То есть computed — это тоже эффект, просто с дополнительной логикой.
3. watchEffect
watchEffect — это практически прямое создание ReactiveEffect.
4. watch
watch строится поверх эффекта, но добавляет:
Но в основе — всё тот же ReactiveEffect.
Scheduler — почему эффект не всегда запускается сразу
ReactiveEffect принимает второй аргумент — scheduler.
Если он передан, при trigger вызывается не run(), а scheduler.
Это позволяет:
Пример:
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 — низкоуровневый инструмент.
Важно:
Это инструмент для продвинутых сценариев:
В обычном приложении достаточно watch и computed.
Как это связано с trackOpBits
В прошлой статье мы разобрали битовые маски.
Теперь становится ясно, где они работают:
ReactiveEffect — это “контейнер выполнения”.
trackOpBits — механизм оптимизации его поведения при глубокой вложенности.
Итог
Если посмотреть на Vue 3 с архитектурной точки зрения, система выглядит так:
Всё остальное — обёртки над этим механизмом.
Понимание ReactiveEffect позволяет видеть реактивность Vue как систему, в которой каждое чтение формирует зависимость, а каждое изменение инициирует конкретное повторное выполнение.
Но тогда мы смотрели на систему через одну конкретную оптимизацию.
Сегодня поднимемся уровнем выше.
Почти всё, что вы делаете во 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.
Что делает 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)
}
}
Здесь важно несколько вещей:
track() работает только если есть activeEffect
Эффект добавляется в dep
Сам эффект сохраняет ссылку на 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():
Эффект становится активным
Любой track() внутри fn() регистрирует именно его
После завершения выполнения активный эффект восстанавливается
Именно так строится стек вложенных эффектов.
Что делает trigger()
Теперь вторая часть механизма.
Когда происходит изменение:
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 как единица исполнения
Теперь становится понятно:
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 как систему, в которой каждое чтение формирует зависимость, а каждое изменение инициирует конкретное повторное выполнение.