Привет, Хабр.
Это моя первая статья здесь. Долгое время не решался что-то публиковать, хотя регулярно читал и разбирал материалы других авторов.
Для первой публикации я выбрал тему внутренней оптимизации реактивности во Vue 3 — trackOpBits и работу ReactiveEffect. Этот механизм почти не заметен при обычной работе с фреймворком, но он напрямую влияет на производительность рендера компонентов и поведение вложенных computed.
В статье разберём, какую проблему решает trackOpBits, как именно он используется внутри системы реактивности и почему эта оптимизация важна в реальных приложениях.
Кратко о ReactiveEffect
В Vue 3 любая реактивная логика завязана на ReactiveEffect:
рендер компонента
computedwatchEffectwatch
Все они внутри — 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.valuetotalWithTax— этоcomputed, у него свой effectВнутри
totalWithTaxчитаетсяtotal.valuetotal— ещё один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’а компонента
Спасибо за внимание.
