Сигналы: алгоритм push-pull
- title
- Сигналы: алгоритм push-pull
- type
- summary
- summary
- Как работают сигналы: реактивность push-pull с автоматическим отслеживанием зависимостей с нуля
- tags
- reactive-programming, signals, javascript, typescript
- sources
- signal-push-pull-algorithm
- created
- 2026-04-06
- updated
- 2026-04-06
- lang
- ru
- translation_of
- signal-push-pull-algorithm
- source_updated
- 2026-04-06
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Вилли Браунер разбирает, как сигналы устроены под капотом, собирая с нуля готовую реализацию примерно на 80 строк TypeScript. Основная идея - двухфазный алгоритм: жадный push для инвалидации и ленивый pull для перевычисления.
Метафора электронных таблиц
Ячейка y = 2 * x автоматически обновляется при изменении x. Это и есть реактивное программирование - изменения в источниках данных распространяются по графу зависимых вычислений без ручной настройки обновлений. JavaScript перенял этот подход начиная с Knockout.js (2010) и RxJS (2012), а сейчас этот паттерн лежит в основе Solid, Vue, Preact, Angular и Svelte.
Сигналы: сторона push
Сигнал хранит значение и множество подписчиков. При записи в сигнал он тут же уведомляет каждого подписчика - это и есть push. Уведомление сообщает "что-то изменилось" (инвалидация), а не "вот новое значение". Подписчики помечаются как dirty, но пока не перевычисляются.
set value(v: T) {
if (value === v) return
value = v
for (const fn of Array.from(subs)) fn(v)
}
Запись того же самого значения пропускается, поэтому установка сигнала в его текущее значение ничего не делает.
Вычисляемые значения: сторона pull
Computed оборачивает функцию, вычисляющую значение на основе одного или нескольких сигналов (или других computed). Он кэширует результат и перевычисляется только при реальном чтении - и только если был помечен как dirty. Это и есть pull. Если computed никто не читает, он вообще не перевычисляется, даже если его зависимости изменились десять раз.
get value() {
if (dirty) _internalCompute()
return cachedValue
}
Флаг dirty служит мостом между push и pull. Push выставляет его, pull сбрасывает.
Автоматическое отслеживание зависимостей
Приём, избавляющий от ручных массивов зависимостей (deps в useEffect у React), - глобальный стек. Когда computed выполняет свою функцию, он кладёт объект контекста в стек. Любой сигнал, прочитанный во время этого выполнения, проверяет стек, находит текущий computed и подписывает его. При повторном запуске computed сначала очищает старые подписки и собирает зависимости с нуля - поэтому условные ветки, переставшие читать сигнал, больше не будут на него подписаны.
// Inside signal's getter:
const currentComputed = STACK[STACK.length - 1]
if (currentComputed) {
subs.add(currentComputed.setDirty)
currentComputed.addSource(() => subs.delete(currentComputed.setDirty))
}
Вложенные computed работают точно так же - computed, читающий другой computed, подписывается на него через тот же механизм стека. Инвалидация распространяется вверх по цепочке: сигнал помечает computed A как dirty, тот помечает dirty computed B и так далее. Но самого перевычисления не происходит, пока кто-нибудь не прочитает самое внешнее значение.
Поток выполнения
const count = signal(1)
const doubleCount = computed(() => count.value * 2)
const plusOne = computed(() => doubleCount.value + 1)
count.value = 5 // push: marks doubleCount dirty, which marks plusOne dirty
console.log(plusOne.value) // pull: plusOne recomputes, which triggers doubleCount to recompute
// doubleCount: 5 * 2 = 10, plusOne: 10 + 1 = 11
Стандартизация в TC39
Предложение tc39-proposal-signals (Stage 1) нацелено на то, чтобы встроить этот механизм напрямую в JavaScript. Идея не в том, чтобы заменить API фреймворков, а в том, чтобы дать общий реактивный примитив под капотом: фреймворки сохраняют собственный внешний API, но используют единый механизм подписок и инвалидации.
Связанные страницы
- reactive-signals - концептуальная страница о паттерне сигналов в целом
- Продакшен-реализации (alien-signals, preact-signals, solidjs-signals) добавляют оптимизации поверх этого базового алгоритма, но основа push-pull остаётся той же.