Сигналы Angular
Сигналы Angular — это система, которая детально отслеживает, как и где используется состояние приложения, позволяя фреймворку оптимизировать обновления отображения.
СОВЕТ: Прежде чем перейти к этому подробному руководству, ознакомьтесь с разделом Основы Angular.
Что такое сигналы?
Сигнал — это обёртка вокруг значения, которая уведомляет заинтересованных потребителей об изменении этого значения. Сигналы могут содержать значения любого типа — от примитивов до сложных структур данных.
Чтобы прочитать значение сигнала, вызовите его функцию-геттер. Это позволяет Angular отслеживать, где используется сигнал.
Сигналы могут быть записываемыми или доступными только для чтения.
Записываемые сигналы
Записываемые сигналы предоставляют API для прямого обновления своих значений. Чтобы создать записываемый сигнал, вызовите функцию signal, передав начальное значение сигнала:
const count = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());
Чтобы изменить значение записываемого сигнала, можно либо .set() его напрямую:
count.set(3);
либо использовать операцию .update(), чтобы вычислить новое значение на основе предыдущего:
// Increment the count by 1. count.update((value) => value + 1);
Записываемые сигналы имеют тип WritableSignal.
Преобразование записываемых сигналов в сигналы только для чтения
WritableSignal предоставляют метод asReadonly(), который возвращает версию сигнала, доступную только для чтения. Это полезно, когда нужно предоставить потребителям доступ к значению сигнала, не позволяя им изменять его напрямую:
@Service()
export class CounterState {
// Private writable state
private readonly _count = signal(0);
readonly count = this._count.asReadonly(); // public readonly
increment() {
this._count.update((v) => v + 1);
}
}
@Component({
/* ... */
})
export class AwesomeCounter {
state = inject(CounterState);
count = this.state.count; // can read but not modify
increment() {
this.state.increment();
}
}
Сигнал только для чтения отражает все изменения исходного записываемого сигнала, но его нельзя изменить с помощью методов set() или update().
ВАЖНО: У сигналов только для чтения нет встроенного механизма, предотвращающего глубокую мутацию их значения.
Вычисляемые сигналы
Вычисляемые сигналы — это сигналы только для чтения, значения которых вычисляются на основе других сигналов. Чтобы определить вычисляемый сигнал, используйте функцию computed и укажите функцию-вычисление:
const count: WritableSignal<number> = signal(0); const doubleCount: Signal<number> = computed(() => count() * 2);
Сигнал doubleCount зависит от сигнала count. Когда значение count обновляется, Angular понимает, что нужно также обновить doubleCount.
Вычисляемые сигналы вычисляются лениво и мемоизируются
Функция-вычисление doubleCount не выполняется до тех пор, пока вы впервые не прочитаете doubleCount. Затем вычисленное значение кэшируется, и при повторном чтении doubleCount возвращается кэшированное значение без повторного вычисления.
Если после этого изменить count, Angular поймёт, что кэшированное значение doubleCount больше не актуально, и при следующем чтении doubleCount вычислит его заново.
Поэтому в вычисляемых сигналах можно безопасно выполнять ресурсоёмкие вычисления, например фильтрацию массивов.
Вычисляемые сигналы не являются записываемыми сигналами
Нельзя напрямую присваивать значения вычисляемому сигналу. То есть выражение
doubleCount.set(3);
вызывает ошибку компиляции, поскольку doubleCount не является WritableSignal.
Зависимости вычисляемых сигналов динамические
Отслеживаются только сигналы, прочитанные непосредственно во время вычисления. Например, в этом computed сигнал count читается, только если сигнал showCount имеет значение true:
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});
При чтении conditionalCount, если showCount имеет значение false, возвращается сообщение «Здесь ничего нет!» — сигнал count при этом не читается. Это означает, что последующее обновление count не приведёт к повторному вычислению conditionalCount.
Если установить для showCount значение true, а затем снова прочитать conditionalCount, вычисление выполнится повторно и перейдёт по ветви, где showCount имеет значение true, вернув сообщение со значением count. После этого изменение count сделает кэшированное значение conditionalCount недействительным.
Обратите внимание: во время вычисления зависимости можно не только добавлять, но и удалять. Если затем снова установить для showCount значение false, то count больше не будет считаться зависимостью conditionalCount.
Реактивные контексты
Реактивный контекст — это состояние во время выполнения, в котором Angular отслеживает чтение сигналов, чтобы установить зависимости. Код, читающий сигнал, — это потребитель, а читаемый сигнал — производитель.
Angular автоматически переходит в реактивный контекст при выполнении следующих действий:
- Выполнение обратного вызова
effectилиafterRenderEffect. - Вычисление сигнала
computed. - Вычисление
linkedSignal. - Вычисление параметров или функции загрузки
resource. - Отрисовка шаблона компонента (включая привязки к свойству host).
Во время этих операций Angular создаёт активную связь. Если отслеживаемый сигнал изменится, Angular в конечном итоге выполнит потребителя повторно.
Проверка реактивного контекста
Angular предоставляет вспомогательную функцию assertNotInReactiveContext, которая проверяет, что код выполняется вне реактивного контекста. Передайте ссылку на вызывающую функцию, чтобы в случае неудачной проверки сообщение об ошибке указывало на соответствующую точку входа API. Так сообщение об ошибке будет понятнее и полезнее, чем стандартное сообщение об ошибке реактивного контекста.
import {assertNotInReactiveContext} from '@angular/core';
function subscribeToEvents() {
assertNotInReactiveContext(subscribeToEvents);
// Safe to proceed - subscription logic here
} Чтение без отслеживания зависимостей
В редких случаях может понадобиться выполнить код, который читает сигналы внутри реактивной функции, например computed или effect, не создавая при этом зависимость.
Например, предположим, что при изменении currentUser нужно записывать в журнал значение counter. Можно создать эффект effect, который читает оба сигнала:
effect(() => {
console.log(`User set to ${currentUser()} and the counter is ${counter()}`);
});
Этот пример записывает сообщение в журнал при изменении любого из сигналов currentUser или counter. Однако если эффект должен выполняться только при изменении currentUser, чтение counter является лишь побочным действием, и изменения counter не должны приводить к записи нового сообщения.
Чтобы чтение сигнала не отслеживалось, вызовите его геттер с помощью untracked:
effect(() => {
console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`);
});
untracked также полезна, если эффекту нужно вызвать внешний код, который не должен считаться зависимостью:
effect(() => {
const user = currentUser();
untracked(() => {
// If the `loggingService` reads signals, they won't be counted as
// dependencies of this effect.
this.loggingService.log(`User set to ${user}`);
});
}); Реактивный контекст и асинхронные операции
Реактивный контекст активен только для синхронного кода. Чтение сигналов, происходящее после асинхронной границы, не будет отслеживаться как зависимость.
effect(async () => {
const data = await fetchUserData();
// Reactive context is lost here - theme() won't be tracked
console.log(`User: ${data.name}, Theme: ${theme()}`);
});
Чтобы гарантировать отслеживание всех операций чтения сигналов, читайте сигналы до await. Это также относится к передаче сигналов в качестве аргументов вызываемой функции с await, поскольку аргументы вычисляются синхронно:
effect(async () => {
const currentTheme = theme(); // Read before await
const data = await fetchUserData();
console.log(`User: ${data.name}, Theme: ${currentTheme}`);
});
effect(async () => {
// Also works: signal is read before await (as function argument)
await renderContent(docContent());
}); Расширенные вычисления
Хотя computed подходит для простых вычислений, доступных только для чтения, иногда может понадобиться записываемое состояние, зависящее от других сигналов. Дополнительные сведения см. в руководстве Зависимое состояние с linkedSignal.
Все API сигналов работают синхронно — signal, computed, input и т. д. Однако приложениям часто приходится работать с данными, доступными асинхронно. Resource позволяет интегрировать асинхронные данные в код приложения на основе сигналов и при этом обращаться к ним синхронно. Дополнительные сведения см. в руководстве Асинхронная реактивность с помощью ресурсов.
Выполнение побочных эффектов для нереактивных API
Для реакции на изменения состояния рекомендуется использовать синхронные или асинхронные вычисления. Однако это подходит не для всех возможных случаев, и иногда нужно реагировать на изменения сигналов в нереактивных API. Для таких ситуаций используйте effect или afterRenderEffect. Дополнительные сведения см. в руководстве Побочные эффекты для нереактивных API.
Чтение сигналов в компонентах OnPush
Когда вы читаете сигнал в шаблоне компонента OnPush, Angular отслеживает его как зависимость этого компонента. При изменении значения сигнала Angular автоматически помечает компонент, чтобы гарантировать его обновление при следующем запуске обнаружения изменений. Дополнительные сведения о компонентах OnPush см. в руководстве Пропуск поддеревьев компонентов.
Дополнительные темы
Функции сравнения сигналов
При создании сигнала можно дополнительно передать функцию сравнения, которая будет проверять, действительно ли новое значение отличается от предыдущего.
import isEqual from 'lodash/isEqual';
const data = signal(['test'], {equal: isEqual});
// Even though this is a different array instance, the deep equality
// function will consider the values to be equal, and the signal won't
// trigger any updates.
data.set(['test']);
Функции сравнения можно передавать как записываемым, так и вычисляемым сигналам.
ПОЛЕЗНО ЗНАТЬ: По умолчанию сигналы используют ссылочное равенство (сравнение с помощью Object.is()).
Проверка типов сигналов
Чтобы проверить, является ли значение сигналом Signal, используйте isSignal:
const count = signal(0); const doubled = computed(() => count() * 2); isSignal(count); // true isSignal(doubled); // true isSignal(42); // false
Чтобы проверить, является ли сигнал записываемым, используйте isWritableSignal:
const count = signal(0); const doubled = computed(() => count() * 2); isWritableSignal(count); // true isWritableSignal(doubled); // false
Использование сигналов с RxJS
Подробные сведения о взаимодействии сигналов и RxJS см. в разделе Взаимодействие RxJS с сигналами Angular.
Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/signals