open atlas
← Все проекты

frontend · intermediate · 3d

Мини-сигналы

Собери реактивную библиотеку сигналов примерно в 100 строк (signal/computed/effect) с автоматическим отслеживанием зависимостей и безглючными батч-обновлениями — та же модель, что лежит в основе Solid, Preact Signals и Vue 3.

Каждый современный реактивный фреймворк использует ту или иную разновидность сигналов. Собирая библиотеку с нуля, ты поймёшь точно, как работает отслеживание зависимостей (трюк с 'currently-executing'), почему в наивных реализациях возникают глюки и как топологическая сортировка и батчинг их исправляют. Сто строк TypeScript, но концепции масштабируются до реактивности Vue и точечных обновлений Solid.

Результат

Библиотека, где цепочки signal → computed → effect обновляются ровно один раз за батч, с тест-сьютом, доказывающим отсутствие устаревших чтений и глюков.

Этапы

0/3 · 0%
  1. 01signal() + effect() с автотрекингом

    Реализуй signal() и effect(): используй глобальную переменную 'currently-executing' для автоматического отслеживания чтений и перезапуска эффектов при записях.

    Критерии готовности
    • Чтение сигнала внутри эффекта авто-подписывает его; запись сигнала перезапускает ровно зависимые эффекты.
    • Эффект, переставший читать сигнал, больше им не перезапускается (зависимости динамические).
  2. 02Ленивый кэшируемый computed()

    Добавь computed(): ленивый, кешированный, пересчитываемый только при реальном изменении исходного сигнала.

    Критерии готовности
    • computed пересчитывается только когда источник реально изменился, иначе кэшируется между чтениями.
    • Чтение computed внутри эффекта отслеживается транзитивно через computed до его источников.
  3. 03Батчинг без глитчей

    Реализуй батчинг, чтобы несколько записей сигнала внутри batch(() => {...}) запускали каждый эффект ровно один раз.

    Критерии готовности
    • Несколько записей внутри batch(() => …) перезапускают каждый эффект ровно один раз, после всех записей.
    • Ромбовидная зависимость (A→B, A→C, B&C→D) обновляет D один раз без устаревшего промежуточного чтения.

Стартер

  • README.md
  • src/signals.ts
  • test/signals.test.ts
Скачать стартер (.zip)

Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: bun test

Рубрика

Джуниор Миддл Сеньор
Механизм отслеживания зависимостей Эффекты перезапускаются явной подпиской (effect.subscribe(signal)); автоматического отслеживания нет — разработчик перечисляет каждую зависимость вручную. Глобальная переменная 'currently-executing' автоматически отслеживает чтения: любой сигнал, прочитанный внутри тела функции эффекта, регистрируется как зависимость без явного перечисления. Зависимости динамические: перед каждым перезапуском предыдущее множество подписчиков очищается, поэтому сигнал, условно читаемый в одной ветке, перестаёт держать эффект подписанным, когда ветка больше не проходит. Ты можешь показать тест, где сигнал, который перестают читать, перестаёт запускать эффект.
Безглючное / топологическое распространение Эффекты перезапускаются немедленно на каждую запись сигнала; ромбовидная зависимость (A→B, A→C, B&C→D) заставляет D запуститься дважды и при первом запуске может прочитать устаревшее промежуточное значение. Несколько записей внутри batch() запускают каждый эффект ровно один раз, после всех записей; D обновляется один раз без устаревшего чтения при изменении A внутри batch. Реактивный граф сортируется топологически перед сбросом, поэтому computed никогда не вычисляется, пока хотя бы один из его источников ещё грязный; ты можешь доказать это ромбовидным тестом и объяснить, почему наивное BFS/push-распространение порождает глюки без топологической сортировки.
Кэширование computed и ленивое вычисление computed() пересчитывается при каждом чтении, независимо от того, изменился ли какой-либо исходный сигнал с момента последнего чтения. computed() ленивый (вычисляется только при чтении) и кэшируемый (возвращает последнее значение, если источник не изменился); пересчитывается только когда помечен грязным записью источника. Чтение computed внутри эффекта отслеживается транзитивно через computed до исходных сигналов, так что эффект подписывается на источники, а не только на узел computed. Ты можешь показать, что изменение источника помечает computed грязным, планирует эффект и computed пересчитывается ровно один раз при следующем сбросе.
Эталонный разбор (спойлер)

Трюк с 'currently-executing': автоматическое отслеживание зависимостей работает через переменную уровня модуля, хранящую текущий выполняющийся эффект (или null). Когда вызывается getter сигнала, он проверяет эту переменную и добавляет эффект в множество подписчиков. Когда эффект завершается, переменная восстанавливается до предыдущего значения. Вложенность работает, потому что переменная сохраняется и восстанавливается при каждом вызове эффекта.

Глюк: в наивном push-реактивном графе запись сигнала A немедленно распространяется на B и C, которые немедленно запускают своих подписчиков. Если D зависит от B и C, он запускается при обновлении B (читая устаревший C) и снова при обновлении C — два вычисления, одно с несогласованным промежуточным состоянием. Топологическая сортировка или батчинг предотвращают это, откладывая весь сброс до тех пор, пока все грязные узлы текущего батча не помечены.

Динамические зависимости: очистка множества подписчиков перед каждым перезапуском необходима для корректности, когда эффекты используют условия. Если эффект читает сигнал A в ветке true и сигнал B в ветке false, и условие меняется, эффект должен отписаться от ветки, которую больше не проходит. Без очистки устаревшие подписки вызывают лишние перезапуски и делают невозможным сборку мусора для эффекта, пока его сигналы живы.

Сделай по-сеньорски

  • Докажи отсутствие глюков: отсортируй реактивный граф топологически перед сбросом, чтобы computed никогда не читался, пока его источники грязные.
  • Добавь untrack() для чтения сигнала без подписки и onCleanup(), чтобы эффекты освобождали ресурсы перед повторным запуском.

Навыки

reactive graph / pull-push modeldependency tracking via execution contexttopological sort for glitch-free updatesbatching

Рекомендуемый стек

typescriptvitest