EnglishРусский Map

Сигналы: алгоритм push-pull

title
Сигналы: алгоритм push-pull
type
summary
summary
Как работают сигналы: реактивность push-pull с автоматическим отслеживанием зависимостей с нуля
tags
reactive-programming, signals, javascript, typescript
created
2026-04-06
updated
2026-04-06
lang
ru
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 остаётся той же.