Сигналы в Angular — как писать более реактивный код

В Angular появилась новая интересная функция: сигналы! Сигналы предоставляют нашему коду новый способ сообщить нашим шаблонам (и другому коду) об изменении наших данных. Это улучшает обнаружение изменений Angular, что также повышает производительность и делает наш код более реактивным.

Зачем нам нужны сигналы?

Начнем с простого примера без использования сигналов. Допустим, вы пишете код для выполнения некоторых основных математических операций.

let x = 5;
let y = 3;
let z = x + y;
console.log(z);

Что этот код записывает в консоль? Да, он выводит 8.

Некоторое время спустя в коде мы меняем значение x. Что z теперь выведет в консоль?

let x = 5;
let y = 3;
let z = x + y;
console.log(z);

x = 10;
console.log(z);

Он все равно выведет 8! Это связано с тем, что значение присваивается z при первом вычислении выражения. Переменная z не реагирует на изменения x или y.

Но мы хотим, чтобы наши переменные реагировали на изменения!

Одна из причин, по которой мы используем Angular, — создание реактивных веб-сайтов, как показано на рисунке 1. Когда пользователь обновляет количество, соответствующие переменные (такие как промежуточный итог и налог) должны реагировать и корректировать затраты. Если пользователь решит удалить товар из корзины, мы снова хотим, чтобы связанные переменные отреагировали и правильно пересчитали стоимость.

b3SbnD_bufoicCX2VGyQiA624LQEC7yIEAVeEj0aVHjxvwmNnTPs-qE565koSuPWUrjAj-UDSw9otj6fXRWHPtr9jce2fnLt8FFAiLP0KRijjpuUiN_cb9lFwe_IbmsSWSzWqV36zBa8Bsnh7ciX4 зо Рисунок 1. Корзина реагирует и производит перерасчет, когда пользователь меняет количество.

Благодаря сигналам наш код может быть более реактивным. Наш предыдущий пример, реализованный с помощью сигналов, будет выглядеть так:

const x = signal(5);
const y = signal(3);
const z = computed(() => x() + y());
console.log(z()); // 8

x.set(10);
console.log(z()); // 13

Вскоре мы рассмотрим этот синтаксис подробно. На данный момент приведенный выше код определяет два сигнала: x и y и дает им начальные значения 5 и 3. Затем мы определяем вычисленный сигнал , z который является суммой x и y. Поскольку сигналы предоставляют уведомления об изменениях, при изменении сигналов x или y любые значения, вычисленные на основе этих сигналов, будут автоматически пересчитываться. Этот код теперь реактивный!

Вычисляемые сигналы реагируют и пересчитываются при изменении любого из зависимых от них сигналов. Если сигнал привязан к шаблону, то при изменении сигнала функция обнаружения изменений Angular автоматически обновляет любое представление, считывающее сигнал. И пользователь видит измененное значение.

Итак, ответ на вопрос «зачем нам сигналы?»:

  • Сигналы обеспечивают большую реактивность
  • Использование сигналов дает нам более точный контроль над обнаружением изменений, что может повысить производительность.

Давайте углубимся в то, что такое сигнал и как он используется.

Что такое сигнал?

Вы можете думать о сигнале как о значении плюс уведомление об изменении. Сигнал — это особый тип переменной, которая содержит значение. Но в отличие от других переменных, сигнал также уведомляет об изменении значения переменной.

Думайте об обычной переменной как о полке, как показано в левой части рисунка 2. Когда переменной присваивается значение, она находится на этой полке. Любой код в пределах области действия может просто прочитать эту переменную на полке.

VNW2DY2fkiBRNox5DIGkh2qr_yRgurq7I3vLumHSqT2ACNKq6I3GiGcMpVvU6f2AImTNIJ3quMh7lzerxfRjD3WBiLPEKBWGRgxGfvsrWpwuvBpvbpllPKJ-lZWHZQLRBguqAHWnITJU3xajiV2BoZ М Рисунок 2. Образно говоря, обычная переменная стоит на полке. Сигнал хранится в поле, которое светится при его изменении.

Сигнал больше похож на коробку, как показано в правой части рисунка 2. Создание сигнала метафорически создает коробку и помещает значение внутрь этой коробки. Поле светится при изменении значения сигнала. Чтобы прочитать сигнал, сначала откройте поле, используя круглые скобки: x(). Технически говоря, мы вызываем функцию получения сигнала, чтобы прочитать сигнал.

Теперь у нас есть ответ на вопрос «что такое сигнал?»:

  • Сигнал — переменная + уведомление об изменении
  • Сигнал реактивен и называется «реактивным примитивом».
  • Сигнал всегда имеет значение
  • Сигнал синхронен
  • Сигнал не является заменой RxJS и Observables для асинхронных операций, таких как http.get

Где мы можем использовать сигналы?

  • Используйте их в компонентах для отслеживания состояния локального компонента.
  • Используйте их в директивах
  • Используйте их в сервисе для обмена состоянием между компонентами.
  • Прочтите их в шаблоне для отображения значений сигналов.
  • Или используйте их где-нибудь еще в своем коде.

Далее давайте рассмотрим, как создавать и использовать сигналы.

Как создать сигнал

Чтобы использовать сигнал, вы сначала его создаете.

quantity = signal<number>(1);

Приведенный выше синтаксис создает и инициализирует сигнал с помощью функции конструктора сигнала.

При необходимости предоставьте параметр универсального типа, чтобы определить тип данных сигнала. Сигналом может быть строка, число, массив, объект или любой тип данных. Во многих случаях тип данных можно определить, и параметр универсального типа не нужен.

Передайте конструктору значение сигнала по умолчанию. Сигнал всегда имеет значение, начиная с значения по умолчанию.

Вот несколько дополнительных примеров:

quantity = signal(1);

qtyAvailable = signal([1, 2, 3, 4, 5, 6]);

selectedVehicle = signal<Vehicle>({ 
  id: 1,
  name: 'AT-AT', 
  price: 19416.13
});

vehicles = signal<Vehicle[]>([]);

Первая строка кода выше создает числовой сигнал со значением по умолчанию 1. Поскольку значение по умолчанию — это число, это quantity сигнал, содержащий число. Параметр универсального типа не требуется.

Вторая строка — это сигнал, содержащий массив чисел. По умолчанию предоставляется массив значений от 1 до 6. Опять же, параметр универсального типа в этом случае не нужен, поскольку его можно вывести из значения по умолчанию.

Сигнал selectedVehicle удерживает Vehicle объект. В этом примере тип невозможно определить, поэтому мы указываем параметр универсального типа Vehicle.

Сигнал vehicles содержит массив Vehicle объектов. По умолчанию это пустой массив. Чтобы строго типизировать массив, мы добавляем параметр универсального типа <Vehicle[]>.

Сигнал, созданный с помощью функции конструктора сигнала, доступен для записи, поэтому вы можете установить для него новое значение, обновить его на основе текущего значения или изменить его содержимое. Вскоре мы увидим примеры этих операций.

После того как вы создали сигнал, вы можете прочитать его значение.

Как читать сигнал

Ранее мы представляли сигнал в виде прямоугольника. Образно говоря, чтобы прочитать значение сигнала, вы должны сначала открыть коробку. Вы делаете это, добавляя круглые скобки, как показано ниже.

quantity();

Начните с имени сигнала и следуйте за ним открывающими и закрывающими скобками. Технически говоря, это вызывает функцию получения сигнала. Функция получения создается «за кулисами» — вы не увидите ее в своем коде.

При работе с Angular сигналы обычно считываются в шаблоне.

<select
    [ngModel]="quantity()"
    (change)="onQuantitySelected($any($event.target).value)">
  <option *ngFor="let q of qtyAvailable()">{{ q }}</option>
</select>

<div>Vehicle: {{ selectedVehicle().name }}</div>
<div>Price: {{ selectedVehicle().price }}</div>
<div [style.color]="color()">Total: {{ totalPrice() }}</div>

В приведенном выше шаблоне отображается поле выбора для выбора количества. Считывает [ngModel] значение сигнала quantity, привязываясь к этому значению.

Привязка события change вызывает onQuantitySelected() метод в компоненте.

Элемент option используется ngFor для перебора каждого элемента массива в qtyAvailable сигнале. Он считывает сигнал и создает выборку option для каждого элемента массива.

Под select элементом находятся три div элемента. Первый читает selectedVehicle сигнал, затем обращается к его name свойству. Второй div элемент считывает selectedVehicle сигнал, а затем отображает его price свойство. Последний div элемент считывает totalPrice сигнал (который мы еще не определили). И он устанавливает цвет текста на значение из color сигнала (которое мы также не определили).

Важно отметить, что при чтении сигнала всегда считывается текущее значение сигнала. Код не знает никаких предыдущих значений сигналов.

Когда пользователь выбирает другую величину элемента select, мы хотим изменить значение сигнала quantity. Таким образом, quantity сигнал становится «источником истины» для выбранного пользователем количества. Давайте посмотрим, как это сделать дальше.

Как изменить значение сигнала

Метод сигнала set заменяет значение сигнала новым значением. По сути, он открывает коробку, удаляет текущий элемент и устанавливает на его место новый элемент.

this.quantity.set(qty);

Распространенным сценарием является изменение значения сигнала в зависимости от действия пользователя. Например:

  • Пользователь выбирает новое количество с помощью select элемента
  • Привязка select события элемента вызывает onQuantitySelected() метод и передает выбранное количество.
  • Действие пользователя обрабатывается в этом обработчике событий внутри компонента.
  • Новое значение устанавливается в quantity сигнал.

Вот пример обработчика событий:

onQuantitySelected(qty: number) {
  this.quantity.set(qty);
}

Всякий раз, когда сигнал устанавливается, код уведомляет всех потребителей об изменении сигнала. В этом контексте потребитель — это любой код, который заинтересован в получении уведомлений об изменениях.

Как потребитель показывает, что он заинтересован в получении уведомлений о конкретном сигнале?

Если код считывает сигнал , этот код уведомляется при изменении сигнала.

Если шаблон считывает сигнал , этот шаблон уведомляется об изменении сигнала и запланирован повторный рендеринг представления.

Таким образом, сам акт чтения сигнала регистрирует интерес потребителя к наблюдению за этим сигналом. Команда Angular называет это золотым правилом компонентов сигналов: «обнаружение изменений для компонента будет запланировано тогда и только тогда, когда сигнал, прочитанный в шаблоне, уведомит Angular о том, что он изменился».

Вот пример, иллюстрирующий процесс. Допустим, в рамках приведенного ниже метода выполняется некоторая работа, требующая корректировки количества. Например, если количество составляет 5 или более, вы получите один бесплатно. Дело в том, что quantity за время выполнения метода сигнал может меняться несколько раз.

onQuantitySelected(qty: number) {
  this.quantity.set(qty);
  
  this.quantity.set(5);
  this.quantity.set(42);
}

Количество отображается в шаблоне с использованием привязки Angular, как показано ниже. Поскольку привязка считывает сигнал quantity, шаблон регистрирует свою заинтересованность в получении уведомлений об изменениях.

{{ quantity() }}

Когда пользователь выбирает количество, onQuantitySelected() метод выполняется. Код в методе сначала устанавливает сигнал на величину, выбранную пользователем. Когда новый сигнал установлен, сигнал генерирует уведомление. На этом этапе запланирован запуск обнаружения изменений Angular. Но у него нет возможности запуститься до тех пор, пока не будет выполнен метод onQuantitySelected().

Метод onQuantitySelected() продолжается, устанавливая сигнал в 5. Сигнал генерирует еще одно уведомление об изменении. Снова функция обнаружения изменений Angular напоминает, что ее необходимо запустить, но она все еще не может быть запущена, поскольку метод onQuantitySelected() все еще выполняется. Затем метод устанавливает сигнал, 42 и процесс повторяется.

Когда onQuantitySelected() метод завершит выполнение, наконец-то можно будет запустить обнаружение изменений Angular. Шаблон считывает сигнал и получает текущее значение этого сигнала, равное 42. Шаблон не знает ни одного из предыдущих значений сигнала. Затем представление повторно визуализируется, и quantity отображается новое значение сигнала.

Если сигнал изменяется, любой потребитель, заинтересованный в чтении этого сигнала, получает уведомление. Но потребителю не предоставляется новая ценность. В следующий раз, когда наступит его очередь выполнения, потребитель считывает текущее значение из сигнала.

Если вы знакомы с RxJS и Observables, сигналы совершенно разные. Сигналы не излучают значения, как это делают Observables. Сигналы не требуют подписки.

Помимо set(), есть еще два способа изменить сигнал: update() и mutate().

Метод set() заменяет сигнал новым значением, метафорически заменяя содержимое поля сигнала.

Передайте новое значение в метод set.

// Replace the value
this.quantity.set(qty);

Метод update() обновляет сигнал на основе его текущего значения. Передайте методу обновления стрелочную функцию. Функция стрелки предоставляет текущее значение сигнала, поэтому вы можете обновить его по мере необходимости. В приведенном ниже коде количество удваивается.

// Update value based on current value
this.quantity.update(qty => qty * 2);

Метод mutate() изменяет содержимое значения сигнала, а не само значение сигнала. Используйте его с массивами для изменения элементов массива и с объектами для изменения свойств объекта. В приведенном ниже коде цена автомобиля увеличивается на 20%.

this.selectedVehicle.mutate(v => v.price = v.price + (v.price * .20));

Независимо от того, как сигнал был изменен, потребители уведомляются о том, что сигнал был изменен. Затем потребители смогут прочитать новое значение сигнала, когда наступит их очередь выполнения.

Как определить вычисляемый сигнал

Часто в нашем коде есть переменные, которые зависят от других переменных. Например, общая цена товара равна цене этого товара, умноженной на желаемое количество этого товара. Если пользователь меняет количество, мы хотим изменить общую цену. Для этого мы используем вычисленные сигналы .

Определите вычисленный сигнал путем вызова вычисленной функции создания. Функция computed() создает новый сигнал, который зависит от других сигналов.

Передайте вычисляемой функции вычислительную функцию, которая выполняет нужную операцию. Операция считывает значение одного или нескольких сигналов для выполнения вычислений.

totalPrice = computed(() => this.selectedVehicle().price * this.quantity());

color = computed(() => this.totalPrice() > 50000 ? 'green' : 'blue');

Первая строка кода выше определяет totalPrice вычисляемый сигнал путем вызова computed() функции создания. Вычислительная функция, переданная в эту вычисляемую функцию, считывает сигналы selectedVehicle и quantity. Если какой-либо сигнал изменится, этот вычисленный сигнал будет уведомлен и обновится, когда наступит его очередь выполнения.

Вторая строка кода определяет color вычисляемый сигнал. Он устанавливает цвет в green зависимости blue от значения сигнала totalPrice. Шаблон может быть привязан к этому сигналу для отображения соответствующего стиля.

Вычисленный сигнал доступен только для чтения. Его нельзя изменить с помощью set(), update() или mutate().

Значение вычисленного сигнала пересчитывается, когда:

  • Изменяется один или несколько его зависимых сигналов.
  • И считывается значение вычисленного сигнала.

Вычисленное значение сигнала запоминается , то есть в нем сохраняется вычисленный результат.

Это вычисленное значение используется повторно при следующем чтении вычисленного значения.

Скажем, например, в нашем шаблоне есть следующее:

Extended price: {{ totalPrice() }}
Total price: {{ totalPrice() }}
Amount due: {{ totalPrice() }}

Когда шаблон впервые считывает totalPrice вычисленный сигнал, значение вычисляется и сохраняется в памяти. Остальные два раза, когда totalPrice сигнал считывается, сохраненное значение используется повторно. Значение не пересчитывается, пока не изменится один из зависимых от него сигналов.

Как использовать эффект

Могут быть случаи, когда вам нужно запустить код при изменении сигнала, и этот код имеет побочные эффекты. Под побочными эффектами я подразумеваю код, который вызывает API или выполняет другую операцию, не связанную с сигналом. В этих случаях вы будете использовать файл effect().

Например, вы хотите отлаживать свои сигналы и выводить значение сигнала каждый раз, когда код реагирует на изменение этого сигнала. Звонок console.log() — это побочный эффект.

Чтобы определить эффект, вызовите effect() функцию создания. Передайте функции операцию, которую необходимо выполнить. Эта операция выполняется повторно каждый раз, когда код реагирует на изменение любого зависимого сигнала.

effect(() => console.log(this.selectedVehicle()));

Функция effect() может быть вызвана внутри другой функции. Поскольку эффект устанавливает своего рода обработчик, он часто вызывается в конструкторе или другом коде запуска.

Альтернативно эффект можно определить декларативно, как показано ниже:

e = effect(() => console.log(this.selectedVehicle()));

Эффект не должен изменять значение каких-либо сигналов. Если вам нужно изменить сигнал на основе изменения зависимого сигнала, вместо этого используйте вычисленный сигнал.

Вы обнаружите, что не будете часто использовать эффекты. Хотя они полезны для регистрации или вызова других внешних API. (Но не используйте их для работы с RxJS и Observables. Будут функции сигналов для преобразования в Observables и обратно.)

Когда использовать сигналы

Вот несколько советов о том, когда использовать сигналы.

Во-первых, продолжайте использовать обработчики событий в компоненте, как сейчас для действий пользователя. Такие действия, как выбор из раскрывающегося списка, нажатие кнопки или ввод в текстовое поле.

Используйте сигнал или вычисленный сигнал в компоненте для любого состояния, которое может измениться. В этом контексте состояние относится к любым данным, которыми управляет компонент. Все, от isLoading флага до текущей отображаемой «страницы» данных и выбранных пользователем критериев фильтра, может быть сигналом. Сигналы особенно полезны при отображении данных в шаблоне, когда эти данные должны реагировать на другие действия.

Поместите общие сигналы в сервисы. Массив транспортных средств, возвращаемый в Observable, можно преобразовать в сигнал. Любые итоговые значения также могут быть сигналами в службе, если эти сигналы распределяются между компонентами.

Продолжайте использовать Observables для асинхронных операций, таких как http.get(). К сигналам добавлено больше функций для сопоставления сигнала с Observable и обратно.

Подведение итогов

Сигналы представляют собой значительное достижение в возможностях реактивного программирования Angular и функциях обнаружения изменений.