Эта статья — логическое продолжение моих экспериментов с реактивностью в JavaScript. На работе я решаю стандартные продуктовые задачи бизнеса. Но чтобы по-настоящему понять, как оптимизировать производительность под капотом, я написал свой реактивный движок достаточно простой для понимания, дебага и тестирования.
В версии движка 2.1.12-beta иерархическая структура побочных эффектов фреймворка опиралась на плоскую модель диспетчеризации. Когда внутри одного эффекта динамически декларировался другой, планировщик ядра сортировал общую очередь флуша исключительно по базовому весу глубины нод (effects.sort((a, b) => a.depth - b.depth)). Такая схема отлично справлялась со стандартными алмазными каскадами, но полностью пасовала перед каноническими тестами на иерархический жизненный цикл эффектов (тесты #237, #238 и #241 из мирового сьюта), где спецификация требует строгого соблюдения контекста владения (Context Ownership) и правила «глубокие очистки выполняются первыми» (deepest first).
Оригинальный тест #237 воссоздаёт следующую схему связей: при перезапуске внешнего эффекта из-за мутации родительского сигнала, старый вложенный эффект обязан быть полностью уничтожен до того, как внешнее тело начнёт собирать новый кадр. На плоском планировщике версии 2.1.12-beta происходила скрытая утечка подписок (Subscription Leak): родительский эффект вызывал своё тело, но старый дочерний эффект оставался висеть в памяти живым. При последующей мутации внутреннего сигнала старый и новый дочерние эффекты просыпались одновременно, накручивая счётчики вызовов (Received: 3 вместо Expected: 2) и вызывая краш консистентности на удалённом сервере.
СХЕМА ТЕСТА #237 (Контекст Владения):
[ Trigger Signal ] ── (мутация) ──→ [ Outer Effect ]
│
(перезапуск тела)
│
▼
[ Stale Inner Effect ] ── (утечка памяти!)Для решения этой проблемы в версии 2.1.13-beta мы внедрили в фабрику createEffect строго типизированное по контракту IEffect изолированное хранилище ownedEffects. Теперь перед каждым новым перезапуском или принудительным уничтожением родительского эффекта, узел временно гасит контекст трекинга зависимостей (engine.activeConsumer = null), пробегается по своим зарегистрированным детям и каскадно вызывает у них метод .dispose(). Это позволило гарантировать атомарное выжигание старых вложенных фреймов строго вне контекста реактивной оценки (фикс теста #40), продвинув движок на единоличное 3-е место в мире.
