Angular

computed: производное состояние

В корзине интернет-магазина есть список товаров, скидка и итоговая сумма с НДС. Раньше итог пересчитывали вручную: в обработчике добавления товара, в обработчике смены скидки, в обработчике удаления. Стоит забыть один из них - и корзина показывает старую сумму. computed убирает эту проблему в корне: разработчик один раз описывает, как считать итог из исходных данных, и Angular сам пересчитывает его при любом изменении зависимостей. Причём только тогда, когда итог реально кому-то нужен, и без лишних промежуточных значений.

  • Корзина и оформление заказа: итоговая сумма, скидки, налоги - производные от списка товаров
  • Фильтрация и сортировка таблиц: видимый список вычисляется из исходных данных и текущих фильтров
  • Формы: признак валидности, текст ошибки и состояние кнопки отправки - производные от значений полей
  • Дашборды: агрегаты (среднее, сумма, максимум) пересчитываются из массива метрик
  • NgRx SignalStore: селекторы выражаются через computed поверх состояния стора

Предварительные знания

  • Понимание signal: создание, чтение через вызов, запись через set и update
  • Идея производного значения: данные, которые вычисляются из других данных
  • Базовое знакомство с чистыми функциями (результат зависит только от входов)

Откуда пришла идея computed

Концепция производных вычисляемых значений с автоматическим отслеживанием зависимостей не нова. Она использовалась в MobX (computed values), Vue (computed properties) и в реактивных системах вроде Knockout ещё в начале 2010-х. Когда команда Angular проектировала сигналы в 2023 году, computed стал естественной парой к signal: один создаёт источник, другой создаёт производное от него. Ключевые свойства - ленивость, мемоизация и отсутствие глитчей - взяты из многолетнего опыта этих библиотек. В Angular computed вышел вместе с сигналами в версии 16 и стабилизировался в 17, а к версии 21 стал стандартным способом выражать любое производное состояние.

computed: автоматическое производное значение

computed принимает функцию вычисления и возвращает сигнал, доступный только для чтения. Внутри функции читаются другие сигналы, и Angular автоматически запоминает, какие именно были прочитаны - это и есть зависимости. Когда любая зависимость меняется, значение computed помечается устаревшим и пересчитывается при следующем чтении. Вручную обновлять computed нельзя: у него нет set и update.

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

Функция computed должна быть чистой: только читать сигналы и возвращать значение. Запись в сигналы или другие побочные эффекты внутри computed - ошибка проектирования. Для побочных эффектов существует effect.

Как computed определяет, от каких сигналов он зависит?

Ленивость и мемоизация

computed обладает двумя важными свойствами производительности. Первое - ленивость: функция вычисления не выполняется до тех пор, пока значение не запросят впервые. Если computed нигде не читается (например, его часть шаблона скрыта через @if), вычисление просто не происходит. Второе - мемоизация: после вычисления результат кэшируется. Повторное чтение без изменения зависимостей возвращает закэшированное значение, а функция повторно не вызывается.

  1. computed создан, но функция ещё не выполнялась - ленивость
  2. Первое чтение запускает вычисление и кэширует результат
  3. Повторное чтение без изменений отдаёт кэш - мемоизация
  4. Изменение зависимости помечает значение устаревшим, и следующее чтение пересчитывает

Сравнение с обычным геттером класса: геттер выполняется при каждом обращении, даже если входные данные не менялись. computed выполняется только когда зависимости реально изменились. Поэтому тяжёлые вычисления (сортировка, агрегация большого массива) в computed дёшевы при повторных чтениях.

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

Обновления без глитчей

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

Достигается это за счёт ленивого пересчёта. Изменение сигнала не запускает немедленный каскад вычислений. Вместо этого зависимые computed лишь помечаются устаревшими. Реальное вычисление происходит при следующем чтении, когда все изменения источников уже применены. Поэтому к моменту чтения c все промежуточные значения согласованы между собой.

  • Система с глитчами — Изменение источника сразу запускает пересчёт зависимых. Читатель может застать момент, когда один источник обновлён, а второй ещё нет
  • Glitch-free (сигналы Angular) — Изменение лишь помечает зависимые устаревшими. Пересчёт откладывается до чтения, когда состояние уже целостно

Свойство glitch-free снимает с разработчика заботу о порядке обновлений. Можно менять несколько сигналов подряд и не бояться, что производные значения покажут несогласованный промежуточный результат. К моменту чтения всё будет цельным.

Что означает свойство glitch-free для производных значений в Angular?

Связь с другими темами

computed - вторая опора сигнальной модели после signal. Дальше идут эффекты и зависимое перезаписываемое состояние:

  • Сигналы — computed читает сигналы как источники и пересчитывается при их изменении
  • effect — Похож тем, что реагирует на изменения, но предназначен для побочных эффектов, а не для возврата значения
  • linkedSignal — Производное состояние, которое в отличие от computed можно перезаписывать вручную

Итог

  • computed создаёт сигнал, доступный только для чтения, значение которого вычисляется из других сигналов
  • Вычисление ленивое: функция не выполняется, пока значение не запросят впервые
  • Результат мемоизируется: при повторном чтении без изменения зависимостей возвращается кэш, функция не вызывается заново
  • Зависимости отслеживаются автоматически по сигналам, прочитанным внутри функции computed
  • Обновления glitch-free: даже если несколько источников меняются вместе, читатель не увидит промежуточного несогласованного состояния

Связанные уроки

  • ng-11-signals-intro — computed строится поверх обычных сигналов и читает их значения
  • ng-13-effect — effect тоже реагирует на изменение сигналов, но запускает побочный эффект, а не возвращает значение
  • ng-14-linked-signal — linkedSignal развивает идею производного состояния, оставляя его перезаписываемым
computed: производное состояние

0

1

Войти