Angular
Сигналы: основа реактивности
Годами Angular полагался на Zone.js: библиотека перехватывала каждый таймер, клик и сетевой запрос, чтобы понять, не пора ли перерисовать приложение. После любого события фреймворк проверял весь дерево компонентов, не зная, что именно изменилось. Сигналы перевернули эту модель. Теперь значение само сообщает, кто от него зависит, и при изменении обновляется только то, что действительно использует это значение. Эта точность позволила Angular 21 сделать zoneless режимом по умолчанию и убрать Zone.js из новых проектов.
- Angular 21 (2025): zoneless стал стабильным и используется по умолчанию, новые приложения создаются без Zone.js
- Сигнальные компоненты: input(), output() и model() строятся на сигналах, заменяя классические декораторы
- NgRx SignalStore: управление состоянием приложения целиком на сигналах вместо классического Redux-подхода
- Крупные дашборды: точечное обновление одной ячейки таблицы вместо проверки всего дерева снижает нагрузку на CPU
- Resource API и httpResource: асинхронная загрузка данных выражается через сигналы состояния запроса
Предварительные знания
- Понимание компонентов Angular и привязки данных в шаблоне
- Базовое представление об обнаружении изменений (change detection)
- Знание разницы между значением и функцией, возвращающей значение
От Zone.js к сигналам
Сигналы вошли в Angular в версии 16 (май 2023) как developer preview и стали стабильными в версии 17. Идея пришла из мира фронтенда, где мелкозернистая реактивность давно использовалась в SolidJS, Vue и Preact Signals. До этого Angular зависел от Zone.js - библиотеки, которая патчила браузерные API, чтобы автоматически запускать обнаружение изменений после любого асинхронного события. Подход работал, но был грубым: фреймворк проверял всё дерево компонентов, не зная источник изменения. Сигналы дали механизм точного отслеживания зависимостей, и к Angular 20-21 это вылилось в стабильный zoneless режим, где Zone.js больше не нужен.
Что такое сигнал и как его читать и писать
Сигнал - это контейнер для значения, который умеет сообщать заинтересованным сторонам о своём изменении. Создаётся он функцией signal с начальным значением. Главная особенность: чтение значения - это вызов функции, а не доступ к свойству. Запись возможна двумя способами: set задаёт новое значение целиком, update вычисляет новое значение на основе текущего.
Почему чтение - это вызов функции? Так Angular понимает момент, когда значение реально запрашивают. Если сигнал прочитан внутри шаблона, computed или effect, фреймворк записывает зависимость и при следующем изменении сигнала обновит ровно эти места. Доступ к обычному свойству такой возможности не даёт.
Частая ошибка новичка - забыть скобки и написать count вместо count(). Без вызова получается ссылка на саму функцию-сигнал, а не его значение. В шаблоне это покажет нечто вроде function signal вместо числа.
Чем set отличается от update при записи в сигнал?
Почему сигналы: мелкозернистость и zoneless
До сигналов Angular работал так: Zone.js перехватывал любое асинхронное событие и сообщал фреймворку, что что-то могло измениться. Дальше Angular проверял всё дерево компонентов сверху вниз, сравнивая привязки. Это надёжно, но затратно: при клике в одном углу страницы проверке подвергалось всё приложение. Сигналы дают точную информацию о зависимостях, поэтому при изменении значения обновляются только те части шаблона, которые это значение читают.
- Модель с Zone.js — Любое событие запускает проверку всего дерева компонентов. Фреймворк не знает, что изменилось, и перепроверяет всё на всякий случай
- Сигнальная модель (zoneless) — Сигнал знает своих читателей. Изменение значения обновляет только зависимые места, без обхода всего дерева и без Zone.js
Эта точность и есть мелкозернистая реактивность. Именно она позволила Angular 21 сделать zoneless режим стандартным: раз фреймворк точно знает, что и когда меняется, грубый перехватчик событий больше не нужен. Новые проекты создаются без Zone.js, а размер бандла и нагрузка на обнаружение изменений снижаются.
Zoneless - это не про то, что приложение стало реактивным магически. Это про то, что обновление инициируют сами сигналы, а не глобальный перехватчик. Меньше ненужных проверок означает меньше работы CPU на каждое взаимодействие.
Как мелкозернистая реактивность сигналов связана с тем, что zoneless стал стандартом в Angular 21?
Перезаписываемые и доступные только для чтения сигналы
Сигнал, созданный через signal(), является перезаписываемым: у него есть методы set и update. Но не всякий сигнал должен быть доступен для записи извне. Сигнал, созданный через computed, доступен только для чтения - его значение полностью определяется источниками. А перезаписываемый сигнал можно превратить в читаемую версию методом asReadonly, чтобы наружу отдать только право чтения.
| Источник | Тип | Можно set/update | Можно читать |
|---|---|---|---|
| signal(value) | WritableSignal | Да | Да |
| computed(fn) | Signal (readonly) | Нет | Да |
| someSignal.asReadonly() | Signal (readonly) | Нет | Да |
| input(value) | InputSignal (readonly) | Нет | Да |
Хороший приём для сервисов: держать перезаписываемый сигнал приватным, а наружу отдавать его через asReadonly. Тогда менять состояние может только сам сервис через методы, а компоненты лишь читают. Это тот же принцип инкапсуляции, что и приватные поля с публичными геттерами.
Сервис хочет, чтобы компоненты могли читать состояние, но меняли его только методы самого сервиса. Какой приём это обеспечивает?
Связь с другими темами
Сигнал - базовый кирпич реактивности. На нём держится остальная сигнальная экосистема:
- computed — Производное значение, которое автоматически пересчитывается при изменении сигналов-источников
- effect — Побочный эффект, который запускается при изменении прочитанных сигналов
- RxJS-интероп — toSignal и toObservable связывают сигналы с потоками RxJS, когда задача требует обоих
Итог
- Сигнал - это обёртка над значением, которая знает, кто его читает, и оповещает зависимых при изменении
- Сигнал создаётся через signal(initialValue), читается вызовом s(), записывается через set(value) или update(fn)
- Чтение сигнала внутри реактивного контекста автоматически регистрирует зависимость - это и есть отслеживание зависимостей
- Мелкозернистая реактивность обновляет только то, что зависит от изменившегося сигнала, что и позволило сделать zoneless стандартом в Angular 21
- Перезаписываемый сигнал из signal() имеет set и update, а доступный только для чтения (из computed или asReadonly) можно лишь читать
Связанные уроки
- ng-12-computed — computed строится поверх сигналов, превращая их в производное состояние
- ng-13-effect — effect реагирует на изменение сигналов и запускает побочные эффекты
- ng-16-rxjs-interop — toSignal соединяет мир RxJS-потоков с сигналами, когда нужны оба подхода