Перейти к содержанию

Angular Signals

Angular Signals — это система, которая детально отслеживает, как и где ваше состояние используется в приложении, позволяя фреймворку оптимизировать обновления рендеринга.

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

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

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

Значение сигнала всегда считывается через функцию getter, что позволяет Angular отслеживать, где используется сигнал.

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

Записываемые сигналы

Записываемые сигналы предоставляют API для обновления их значений напрямую. Вы создаете записываемые сигналы, вызывая функцию signal с начальным значением сигнала:

1
2
3
const count = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());

Чтобы изменить значение записываемого сигнала, вы можете либо .set() его непосредственно:

1
count.set(3);

или использовать операцию .update() для вычисления нового значения на основе предыдущего:

1
2
// Increment the count by 1.
count.update((value) => value + 1);

При работе с сигналами, содержащими объекты, иногда полезно мутировать объект напрямую. Например, если объект представляет собой массив, вы можете захотеть вставить новое значение, не заменяя массив полностью. Для такого внутреннего изменения используйте метод .mutate:

1
2
3
4
5
6
7
const todos = signal([
    { title: 'Learn signals', done: false },
]);
todos.mutate((value) => {
    // Change the first TODO in the array to 'done: true' without replacing it.
    value[0].done = true;
});

Записываемые сигналы имеют тип WritableSignal.

Вычисляемые сигналы

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

1
2
3
4
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(
    () => count() * 2
);

Сигнал doubleCount зависит от count. Когда count обновляется, Angular знает, что все, что зависит от count или doubleCount, также должно обновиться.

Вычисления как лениво оцениваются, так и мемоизируются

Функция вычисления doubleCount не запускается для вычисления своего значения до тех пор, пока doubleCount не будет прочитан в первый раз. После вычисления это значение кэшируется, и последующие чтения doubleCount будут возвращать кэшированное значение без пересчета.

Когда count изменяется, он сообщает doubleCount, что его кэшированное значение больше не действительно, и значение пересчитывается только при следующем чтении doubleCount.

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

Вычислимые сигналы не являются сигналами, доступными для записи

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

1
doubleCount.set(3);

выдает ошибку компиляции, поскольку doubleCount не является WritableSignal.

Вычисленные зависимости сигналов являются динамическими

Отслеживаются только те сигналы, которые действительно считываются во время вычисления. Например, в этом вычислении сигнал count читается только условно:

1
2
3
4
5
6
7
8
9
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, сообщение "Nothing to see here!" возвращается без чтения сигнала count. Это означает, что обновление count не приведет к повторному вычислению.

Если позже showCount будет установлен в true и conditionalCount будет прочитан снова, деривация будет выполнена заново и возьмет ветвь, где showCount будет true, возвращая сообщение, которое показывает значение count. Изменения в count затем аннулируют кэшированное значение conditionalCount.

Обратите внимание, что зависимости могут быть как удалены, так и добавлены. Если позже showCount снова будет установлен в false, то count больше не будет считаться зависимостью conditionalCount.

Чтение сигналов в компонентах OnPush

Когда компонент OnPush использует значение сигнала в своем шаблоне, Angular будет отслеживать сигнал как зависимость этого компонента. Когда сигнал обновляется, Angular автоматически помечает компонент, чтобы обеспечить его обновление при следующем запуске обнаружения изменений. Дополнительную информацию о компонентах OnPush см. в руководстве Skipping component subtrees.

Эффекты

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

1
2
3
effect(() => {
    console.log(`The current count is: ${count()}`);
});

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

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

Применение эффектов

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

  • Протоколирование отображаемых данных и их изменения, либо для аналитики, либо в качестве инструмента отладки.

  • синхронизация данных с window.localStorage.

  • Добавление пользовательского поведения DOM, которое не может быть выражено с помощью синтаксиса шаблонов.

  • Выполнение пользовательского рендеринга в <canvas>, библиотеку диаграмм или другую стороннюю библиотеку пользовательского интерфейса.

Когда не следует использовать эффекты

Избегайте использования эффектов для распространения изменений состояния. Это может привести к ошибкам ExpressionChangedAfterItHasBeenChecked, бесконечным циклическим обновлениям или ненужным циклам обнаружения изменений.

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

Контекст инъекции

По умолчанию для регистрации нового эффекта с помощью функции effect() требуется "контекст инъекции" (доступ к функции inject). Самый простой способ обеспечить это — вызвать effect в компоненте, директиве или конструкторе сервиса:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Component({...})
export class EffectiveCounterCmp {
    readonly count = signal(0);
    constructor() {
        // Register a new effect.
        effect(() => {
            console.log(`The count is: ${this.count()})`);
        });
    }
}

В качестве альтернативы эффект может быть назначен полю (что также дает ему описательное имя).

1
2
3
4
5
6
7
8
@Component({...})
export class EffectiveCounterCmp {
    readonly count = signal(0);

    private loggingEffect = effect(() => {
        console.log(`The count is: ${this.count()})`);
    });
}

Чтобы создать эффект вне конструктора, вы можете передать Injector в effect через его опции:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component({...})
export class EffectiveCounterCmp {
    readonly count = signal(0);
    constructor(private injector: Injector) {}

    initializeLogging(): void {
        effect(
            () => {
                console.log(
                    `The count is: ${this.count()})`
                );
            },
            { injector: this.injector }
        );
    }
}

Уничтожение эффектов

Когда вы создаете эффект, он автоматически уничтожается, когда уничтожается его объемлющий контекст. Это означает, что эффекты, созданные внутри компонентов, уничтожаются при уничтожении компонента. То же самое относится к эффектам внутри директив, сервисов и т.д.

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

Расширенные темы

Функции равенства сигналов

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

1
2
3
4
5
6
7
import _ from 'lodash';
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']);

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

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

Чтение без отслеживания зависимостей

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

Например, предположим, что когда currentUser меняется, значение counter должно быть зарегистрировано. Создайте effect, который считывает оба сигнала:

1
2
3
effect(() => {
  console.log(`User set to `${currentUser()}` and the counter is ${counter()}`);
});

Этот пример регистрирует сообщение, когда изменяется либо currentUser, либо counter. Однако, если эффект должен выполняться только при изменении currentUser, то чтение counter является случайным и изменения counter не должны регистрировать новое сообщение.

Вы можете предотвратить отслеживание чтения сигнала, вызвав его геттер с untracked:

1
2
3
effect(() => {
  console.log(`User set to `${currentUser()}` and the counter is ${untracked(counter)}`);
});

untracked также полезен, когда эффект должен вызвать внешний код, который не должен рассматриваться как зависимость:

1
2
3
4
5
6
7
8
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}`);
    });
});

Функции очистки эффектов

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
effect((onCleanup) => {
    const user = currentUser();

    const timer = setTimeout(() => {
        console.log(
            `1 second ago, the user became ${user}`
        );
    }, 1000);

    onCleanup(() => {
        clearTimeout(timer);
    });
});

Комментарии