Привет.
Это моя первая статья здесь. Долгое время не решался что-то публиковать, хотя регулярно читал и разбирал материалы других авторов.
Для первой публикации я выбрал тему внутренней оптимизации реактивности во Vue 3 — trackOpBits и работу ReactiveEffect. Этот механизм почти не заметен при обычной работе с фреймворком, но он напрямую влияет на производительность рендера компонентов и поведение вложенных computed.
В статье разберём, какую проблему решает trackOpBits, как именно он используется внутри системы реактивности и почему эта оптимизация важна в реальных приложениях.
Кратко о ReactiveEffect
В Vue 3 любая реактивная логика завязана на ReactiveEffect:
-
рендер компонента
-
computed -
watchEffect -
watch
Все они внутри — effect’ы.
Упрощённо:
class ReactiveEffect {
fn
deps = []
active = true
trackOpBits = 0
}
Во время выполнения fn effect становится активным, и все обращения к реактивным данным регистрируются как зависимости.
Где возникает реальная проблема
Посмотрим на реальный сценарий, который происходит в каждом Vue-приложении.
Пример: компонент + computed + computed
const state = reactive({
price: 100,
count: 2,
tax: 0.2
})
const total = computed(() => {
return state.price * state.count
})
const totalWithTax = computed(() => {
return total.value * (1 + state.tax)
})
А теперь представим компонент:
const Comp = {
setup() {
return () => {
return h('div', totalWithTax.value)
}
}
}
Что реально происходит при первом рендере:
-
Создаётся render effect компонента
-
Внутри рендера читается
totalWithTax.value -
totalWithTax— этоcomputed, у него свой effect -
Внутри
totalWithTaxчитаетсяtotal.value -
total— ещё одинcomputed, ещё один effect -
Внутри
totalчитаются: state.price, state.count
Итого, мы имеем вложенность effect’ов глубиной 3:
render effect
└─ computed(totalWithTax)
└─ computed(total)
└─ reactive state
Что пойдёт не так без оптимизаций
Наивная реализация реактивности делала бы следующее:
-
каждый
get: добавляетactiveEffectвdep -
каждый
effect: при новом запуске очищает всеdepsи пересобирает их заново
При вложенных effect’ах это означает:
-
повторные добавления одного и того же effect’а
-
лишние проверки
-
постоянные
cleanupдаже там, где зависимости не менялись
На больших деревьях компонентов и сложных computed это быстро становится дорогой операцией.
effectTrackDepth — контроль глубины
Во Vue 3 есть глобальный счётчик:
let effectTrackDepth = 0
Каждый раз, когда начинается выполнение effect’а:
effectTrackDepth++
А при завершении — уменьшается.
Это позволяет Vue понимать, на каком уровне вложенности сейчас идёт сбор зависимостей.
Что такое trackOpBits
trackOpBits — это битовая маска, хранящая информацию о том,
на каких уровнях глубины effect уже был зарегистрирован в зависимостях.
Для текущей глубины вычисляется бит:
const trackOpBit = 1 << effectTrackDepth
Этот бит используется как флаг.
Как это работает на практике
Когда выполняется track(dep):
-
Vue проверяет: есть ли у effect’а
trackOpBitдля текущей глубины -
Если бит уже установлен: effect не добавляется повторно в
dep -
Если бита нет: effect добавляется и выставляется бит
if (!(effect.trackOpBits & trackOpBit)) {
dep.add(effect)
effect.trackOpBits |= trackOpBit
}
Таким образом:
-
один и тот же effect не может быть добавлен дважды
-
Vue избегает лишних операций при вложенных вычислениях
Почему это особенно важно для computed
computed во Vue 3:
-
ленивые
-
кешируемые
-
могут вызываться из других
computedи из рендера
Без trackOpBits каждый доступ к .value во вложенных цепочках приводил бы к:
-
повторному трекингу
-
очистке зависимостей
-
лишним аллокациям
С битовой маской:
-
зависимости собираются один раз на уровень
-
повторные чтения становятся почти бесплатными
Ограничение по глубине
Во Vue 3 есть ограничение на максимальную глубину, где используется битовая оптимизация
(на момент написания — 30 уровней).
После этого Vue аккуратно откатывается к более простой логике трекинга, без битов. Это сделано, чтобы:
-
избежать переполнения битовой маски
-
сохранить предсказуемое поведение
На практике в обычных приложениях до этого лимита почти никогда не доходят.
Почему этого не было во Vue 2
Во Vue 2:
-
реактивность строилась на
Object.defineProperty -
не было
ReactiveEffectв текущем виде -
не было чёткого контроля вложенности эффектов
Архитектура Vue 3 (Proxy + эффекты) позволила:
-
отслеживать глубину
-
использовать битовые маски
-
минимизировать работу GC и аллокации
trackOpBits — пример оптимизации, которая стала возможной только после полной переработки реактивности.
Нужно ли это знать обычному разработчику
Скорее нет — Vue отлично работает и без этого знания.
Но если вы:
-
дебажите странные перерендеры
-
пишете сложные
computed -
работаете с производительностью
-
или просто хотите понимать, что происходит под капотом
— знание таких деталей сильно упрощает о поведении фреймворка.
Заключение
trackOpBits — маленькая, но очень важная часть реактивности Vue 3.
Она позволяет:
-
эффективно работать с вложенными effect’ами
-
избежать лишнего трекинга
-
сделать
computedи рендер компонентов действительно быстрыми
Именно такие низкоуровневые решения создают ощущение, что Vue 3 «просто летает», даже в больших приложениях.
Если тема будет интересна — можно отдельно разобрать:
-
scheduler эффектов
-
очереди
pre / post flush -
или жизненный цикл рендер effect’а компонента
Спасибо за внимание.
Автор: wisead
