Angular

Под капотом: граф реактивности и будущее Angular

Сигнал меняется - и через мгновение обновляется ровно тот фрагмент интерфейса, который от него зависит, ни одним вычислением больше. Промежуточные computed не пересчитываются по два раза, устаревшие значения не мелькают на экране. За этой аккуратностью стоит конкретная структура данных - направленный граф producer-consumer, по которому изменения распространяются уведомлениями, а пересчёт происходит лениво и без сбоев. Это финал курса: разобрать, как реактивность Angular устроена изнутри, и что нового в версии 22 (июнь 2026) и куда фреймворк движется дальше.

  • Reactivity-граф сигналов - общая идея современных фреймворков (Angular, SolidJS, Vue), а не частность Angular
  • Glitch-free семантика гарантирует, что пользователь никогда не увидит промежуточное несогласованное состояние computed
  • Zoneless-планировщик (по умолчанию с Angular 21) убирает Zone.js, опираясь на уведомления графа сигналов о необходимости проверки
  • tsgo (компилятор TypeScript на Go) обещает ускорение сборки в 5-10 раз, продолжая курс на быстрые движки
  • Что в Angular v22 (и дальше): OnPush по умолчанию, signal-first API, инструменты, дружественные к AI-ассистентам

Предварительные знания

  • Обнаружение изменений и zoneless-режим (предыдущий урок модуля)
  • Сигналы: signal, computed, effect и их базовое использование
  • Понятие направленного графа (узлы и рёбра, направление зависимостей)
  • Обнаружение изменений и zoneless
  • Введение в сигналы

От Zone.js к реактивному графу

С первых версий Angular узнавал об изменениях через Zone.js - библиотеку, патчившую все асинхронные API браузера, чтобы после любого таймера, события или промиса запустить проверку всего дерева компонентов. Это работало, но было затратно и грубо: проверялось много лишнего. В Angular 16 (2023) команда во главе с Alex Rickabaugh ввела сигналы как точную реактивную примитиву на основе графа producer-consumer. Сигнал точно знает, кто от него зависит, поэтому проверять можно только затронутое. Это открыло путь к zoneless-режиму, стабильному к Angular 21, где Zone.js больше не нужен.

Граф producer-consumer

В основе сигналов лежит направленный граф зависимостей. Его узлы делятся на две роли. Producer - источник значения, на который можно подписаться: writable-сигнал (signal) или вычисляемый (computed). Consumer - тот, кто читает значение и зависит от него: computed (он одновременно и producer, и consumer) или реактивный контекст вроде effect и шаблона компонента. Рёбра графа - это зависимости: ребро от producer к consumer означает consumer читает этот producer.

Ключевая деталь - граф строится автоматически и динамически. Когда computed выполняет свою функцию, любое чтение другого сигнала внутри неё регистрируется как зависимость на лету. Не нужно вручную перечислять, от чего зависит вычисление: фреймворк отслеживает фактические чтения во время выполнения. Если в этот раз ветка кода не прочитала какой-то сигнал, зависимости от него на этот раз не возникнет.

Динамическое отслеживание делает граф точным. Пока useTax() ложно, computed total не зависит от taxRate, и изменение taxRate его не затронет. Как только useTax станет истинным, при следующем пересчёте чтение taxRate зарегистрирует новую зависимость. Граф всегда отражает реальные чтения текущего прохода, а не объявленные заранее.

Как в графе сигналов определяется, от каких producer зависит computed?

Push-уведомление и pull-вычисление

Реактивность Angular гибридная: уведомления идут push, а вычисление - pull. При изменении writable-сигнала он не пересчитывает зависимых сразу. Вместо этого по графу распространяется лёгкое уведомление вниз: все транзитивные consumer помечаются как возможно устаревшие (dirty). Это дёшево - просто пометки, без вычислений. Реальный пересчёт откладывается до момента, когда значение действительно понадобится.

Pull-фаза происходит при чтении. Когда consumer (например, шаблон при следующем обнаружении изменений) читает computed, тот проверяет: помечен ли я устаревшим. Если да, он пересчитывается и кеширует результат; если нет, возвращает кешированное значение мгновенно. Так вычисляется только то, что и помечено устаревшим, и реально прочитано. Изменение, которое никто не читает, не стоит ничего.

Разделение на push и pull даёт сразу два свойства. Push дёшев, потому что лишь помечает, и распространяется по точным рёбрам графа. Pull ленив, поэтому отсекает вычисления, результат которых никому не нужен. Вместе они дают модель, где стоимость реактивности пропорциональна реально используемым данным, а не числу изменений.

Что происходит сразу после вызова writable-сигнала set, до того как кто-либо прочитает зависимый computed?

Glitch-free и мемоизация

Наивная реактивность страдает от глитчей - промежуточных несогласованных состояний. Представим ситуацию: computed c зависит и от a, и от b, причём b тоже вычисляется из a. Если изменить a и пересчитать c раньше, чем обновится b, c на миг увидит новое a и старое b. Граф сигналов Angular устроен так, чтобы это было невозможно: семантика glitch-free гарантирует, что за один проход consumer никогда не видит рассогласованных значений и не пересчитывается дважды.

Достигается это версионированием и отложенным pull-пересчётом. Каждый producer хранит версию значения; consumer запоминает версии своих зависимостей на момент последнего вычисления. При pull computed сначала проверяет, изменились ли версии зависимостей, и пересчитывается лишь при реальном изменении. Поскольку всё вычисляется лениво и сверху вниз по запросу, к моменту чтения c все его зависимости уже приведены к согласованному состоянию.

Мемоизация - вторая половина эффективности. Computed кеширует результат и при следующем чтении возвращает его без пересчёта, если версии зависимостей не менялись. Более того, если зависимость пересчиталась, но дала прежнее значение (equality-проверка), уведомление дальше по графу не идёт - зачем пересчитывать consumer, если вход не изменился. Это отсекает каскады лишних обновлений.

Equality-проверка по умолчанию использует Object.is, но её можно задать своим компаратором через опцию equal у signal и computed. Это полезно для структур, где ссылочное сравнение даёт ложные срабатывания: свой компаратор остановит ненужное распространение по графу при семантически равных значениях.

Что гарантирует glitch-free семантика графа сигналов?

Zoneless-планировщик и будущее Angular

Точный граф сигналов сделал возможным zoneless-режим. Раньше Angular узнавал об изменениях через Zone.js, патчивший все асинхронные API, и после любого таймера или события прогонял проверку всего дерева компонентов. В zoneless этого нет: когда сигнал, прочитанный в шаблоне, меняется, он уведомляет планировщик, что соответствующий компонент нужно проверить. Планировщик объединяет такие запросы и запускает обнаружение изменений один раз за кадр - только для затронутых компонентов.

  • Zone.js (прошлое) — Патчит все async API. После любого события проверяет всё дерево компонентов сверху вниз. Грубо и затратно, много лишних проверок.
  • Zoneless-планировщик (Angular 21) — Сигналы сами уведомляют о необходимости проверки. Планировщик батчит запросы и проверяет только затронутые компоненты раз за кадр. Точно и дёшево.

Это объясняет требование явного detectChanges в zoneless-тестах из урока о тестировании: без Zone.js нет неявного перехвата асинхронности, и проверку запускают уведомления графа либо явный вызов. Тот же механизм лежит в основе курса Angular на signal-first архитектуру - чем больше состояния выражено сигналами, тем точнее планировщик знает, что и когда проверять.

v22 и дальшеЧто меняетсяЗачем
OnPush по умолчаниюКомпоненты проверяются по сигналам, а не каждый циклМеньше лишних проверок из коробки
Signal-first APIСигналы как основной способ состояния, inputs, queriesТочный граф реактивности по умолчанию
tsgo (TypeScript на Go)Проверка типов и компиляция в 5-10 раз быстрееСборка перестаёт быть узким местом
AI-дружественные инструментыСхемы, генераторы и API, понятные ассистентамКодогенерация и рефакторинг с участием AI

Складывается единая картина. Граф producer-consumer дал точную реактивность, точная реактивность открыла zoneless, zoneless подталкивает OnPush по умолчанию и signal-first API, а параллельная линия движков (esbuild, Vite, на горизонте tsgo) убирает медленные части сборки. Angular версии 22 (июнь 2026) и дальше - это фреймворк, где состояние по умолчанию выражено сигналами, проверяется минимально и собирается быстро.

Практический вывод капстоуна: чем больше состояния выражено через signal и computed, тем меньше работы достаётся обнаружению изменений и тем предсказуемее ведёт себя приложение в zoneless. Понимание графа producer-consumer - это не академическая деталь, а основа для проектирования производительных интерфейсов на современном Angular.

Как zoneless-планировщик узнаёт, какие компоненты нужно проверить, без Zone.js?

Связь с другими темами

Финальный урок собирает internals воедино. Связи:

  • Обнаружение изменений и zoneless — Здесь раскрывается, как именно граф сигналов заменяет обход всего дерева
  • Введение в сигналы — Это внутреннее устройство примитивов, с которыми знакомил вводный урок
  • Сборка и tsgo — Ускорение сборки на Go продолжает линию esbuild из урока о CLI

Итог

  • Сигналы образуют направленный граф producer-consumer: producer (signal, computed) знает своих consumer, consumer знает свои зависимости
  • Изменение распространяется push-уведомлением (зависимые помечаются устаревшими), а пересчёт ленивый - pull при следующем чтении
  • Computed мемоизирует значение и пересчитывается только если реально менялась хотя бы одна зависимость
  • Glitch-free: за один цикл consumer не видит промежуточных несогласованных состояний и не пересчитывается дважды
  • Zoneless-планировщик заменяет Zone.js: проверка запускается по уведомлениям графа, а не по патчингу всех async API
  • Angular v22 и дальше: OnPush по умолчанию, signal-first API, ускорение сборки через tsgo и инструменты, дружественные к AI

Связанные уроки

  • ng-21-change-detection-zoneless — Урок раскрывает изнутри механизм обнаружения изменений и zoneless из предыдущего материала
  • ng-11-signals-intro — Это углубление введения в сигналы: здесь разбирается их внутренний граф реактивности
  • ng-43-cli-build-internals — tsgo продолжает линию ускорения сборки из урока о CLI и esbuild
Под капотом: граф реактивности и будущее Angular

0

1

Войти