frontend · intermediate · 3d
Мини-сигналы
Собери реактивную библиотеку сигналов примерно в 100 строк (signal/computed/effect) с автоматическим отслеживанием зависимостей и безглючными батч-обновлениями — та же модель, что лежит в основе Solid, Preact Signals и Vue 3.
Результат
Библиотека, где цепочки signal → computed → effect обновляются ровно один раз за батч, с тест-сьютом, доказывающим отсутствие устаревших чтений и глюков.
Этапы
0/3 · 0%- 01signal() + effect() с автотрекингом
Реализуй signal() и effect(): используй глобальную переменную 'currently-executing' для автоматического отслеживания чтений и перезапуска эффектов при записях.
Критерии готовности- Чтение сигнала внутри эффекта авто-подписывает его; запись сигнала перезапускает ровно зависимые эффекты.
- Эффект, переставший читать сигнал, больше им не перезапускается (зависимости динамические).
- 02Ленивый кэшируемый computed()
Добавь computed(): ленивый, кешированный, пересчитываемый только при реальном изменении исходного сигнала.
Критерии готовности- computed пересчитывается только когда источник реально изменился, иначе кэшируется между чтениями.
- Чтение computed внутри эффекта отслеживается транзитивно через computed до его источников.
- 03Батчинг без глитчей
Реализуй батчинг, чтобы несколько записей сигнала внутри batch(() => {...}) запускали каждый эффект ровно один раз.
Критерии готовности- Несколько записей внутри batch(() => …) перезапускают каждый эффект ровно один раз, после всех записей.
- Ромбовидная зависимость (A→B, A→C, B&C→D) обновляет D один раз без устаревшего промежуточного чтения.
Стартер
- README.md
- src/signals.ts
- test/signals.test.ts
Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: 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(), чтобы эффекты освобождали ресурсы перед повторным запуском.