Signals: реактивность нового поколения
Сигнал — это коробка со значением, которая знает, кто на неё смотрит, и сама будит зависимых, когда содержимое меняется.
«Раньше Angular проверял всё подряд на всякий случай. Сигналы говорят ему точно: вот это изменилось, обнови только это».
Сигналы — главное нововведение современного Angular и будущее его реактивности. Сигнал хранит значение и оповещает всех, кто от него зависит, когда значение меняется. Создаётся он функцией signal(), читается вызовом, а пишется через set или update:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Счёт: {{ count() }}</p>
<button (click)="inc()">+1</button>
<button (click)="reset()">Сброс</button>
`,
})
export class CounterComponent {
count = signal(0); // создаём сигнал
inc() { this.count.update(n => n + 1); } // на основе старого
reset() { this.count.set(0); } // напрямую
}
Заметьте: в шаблоне мы пишем count() — со скобками, ведь чтение сигнала это вызов функции. Когда сигнал в шаблоне меняется, Angular точно знает, какой кусок DOM обновить, без проверки всего дерева компонентов. Это делает приложение быстрее и предсказуемее.
Как работает под капотом
Когда шаблон читает count(), Angular запоминает: «этот участок DOM зависит от сигнала count». Это называется отслеживание зависимостей. При вызове set/update сигнал помечает зависимых как «грязных» и планирует их обновление. Никаких ручных подписок и отписок — граф зависимостей строится автоматически.
signal(0) <- коробка со значением + список читателей
|
шаблон читает count() -> подписался автоматически
|
count.set(5)
|
сигнал: "я изменился" -> будит читателей -> обновить DOM-узел
Запускаемая врезка: реактивный сигнал на замыканиях
Соберём сигнал с нуля на чистом JS — это и есть его суть. «Попробуй сам ▶».
// сигнал: значение + подписчики, оповещаемые при изменении
function signal(initial) {
let value = initial;
const subscribers = new Set();
function read() { return value; }
read.set = (v) => {
if (v === value) return; // не изменилось — молчим
value = v;
subscribers.forEach(fn => fn(v)); // будим зависимых
};
read.update = (fn) => read.set(fn(value));
read.subscribe = (fn) => subscribers.add(fn);
return read;
}
const log = [];
const count = signal(0);
count.subscribe(v => log.push('DOM: счёт = ' + v));
count.update(n => n + 1); // 1
count.update(n => n + 1); // 2
count.set(2); // то же значение — оповещения не будет
console.log('текущее:', count()); // 2
console.log(log); // ["DOM: счёт = 1", "DOM: счёт = 2"]
Частые ошибки
- Забыть скобки.
{{ count }}выведет функцию; правильно{{ count() }}. - Мутировать значение напрямую. Для объектов/массивов меняйте через
updateс новой ссылкой, иначе сигнал не заметит изменения. - Звать
setв цикле без нужды. Каждое изменение планирует обновление; группируйте логику.
Best practices
- Используйте
update, когда новое значение зависит от старого, иset— когда нет. - Держите в сигналах неизменяемые данные: новый объект вместо мутации старого.
- Сигналы — предпочтительный способ хранить состояние компонента в современном Angular.
Итоги. Сигнал хранит значение и сам оповещает зависимых. signal() создаёт, () читает, set/update пишут. Это точечная реактивность вместо тотальной проверки. Дальше — производные значения через computed.
Закрепляем
Сигнал — это контейнер значения, который знает своих читателей и сам оповещает их при изменении. Эта простая идея переворачивает реактивность Angular: вместо того чтобы периодически проверять всё дерево компонентов «на всякий случай», фреймворк точно знает, что именно изменилось и какой участок DOM нужно обновить. Отсюда и скорость, и предсказуемость. Запомните троицу операций: signal() создаёт, вызов со скобками () читает, а set и update записывают.
Важнейшая привычка при работе с сигналами — иммутабельность для объектов и массивов. Сигнал замечает изменение по смене ссылки, а не по содержимому. Если вы мутируете массив на месте через push или меняете поле объекта напрямую, ссылка остаётся прежней, и сигнал «не видит» изменения — зависимые computed и шаблоны не обновятся. Поэтому всегда создавайте новый массив или объект: update(list => [...list, item]) вместо list.push(item). Эта дисциплина поначалу кажется лишней, но именно она делает реактивность надёжной.
| Операция | Назначение |
|---|---|
| signal(v) | Создать сигнал |
| count() | Прочитать значение |
| count.set(v) | Записать новое значение |
| count.update(fn) | Изменить на основе старого |