computed: производные значения
computed — это сигнал, который ничего не хранит, а вычисляет себя из других сигналов и пересчитывается ровно тогда, когда нужно.
«Не храните то, что можно вывести. Цена с учётом скидки — не данные, а формула».
Часть состояния не нужно хранить — её можно вычислить из другого. Сумма корзины выводится из списка товаров; полное имя — из имени и фамилии. Для таких производных значений есть computed(): он создаёт сигнал, формула которого зависит от других сигналов и пересчитывается автоматически.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart-summary',
standalone: true,
template: `
<p>Товаров: {{ count() }}</p>
<p>Сумма: {{ total() }} ₽</p>
<p>К оплате: {{ withDiscount() }} ₽</p>
`,
})
export class CartSummaryComponent {
prices = signal([2990, 1990, 500]);
count = computed(() => this.prices().length);
total = computed(() => this.prices().reduce((a, b) => a + b, 0));
withDiscount = computed(() => Math.round(this.total() * 0.9));
}
Здесь withDiscount зависит от total, а total — от prices. Измените prices — и вся цепочка пересчитается сама. Computed-сигнал доступен только для чтения: его нельзя set, ведь его значение полностью определяется источниками.
Как работает под капотом
Computed обладает двумя свойствами: он ленив (формула не выполняется, пока значение не прочитают) и мемоизирован (запоминает результат и пересчитывает только если изменилась хотя бы одна зависимость). Angular строит граф: знает, что withDiscount зависит от total, а тот от prices. При изменении источника помечаются «грязными» только затронутые узлы.
prices (signal)
|
v
total = computed(...) пересчёт только при смене prices
|
v
withDiscount = computed(...) пересчёт только при смене total
|
v
шаблон читает withDiscount() -> ленивое вычисление + мемо
Запускаемая врезка: computed с мемоизацией на JS
Покажем ленивость и мемо «руками». «Попробуй сам ▶».
function signal(v){ const s=()=>s.v; s.v=v; s.set=x=>{s.v=x; s.dirty&&s.dirty();}; return s; }
function computed(fn, deps){
let cached, valid = false;
deps.forEach(d => { d.dirty = () => { valid = false; }; });
return () => {
if (!valid) { cached = fn(); valid = true; console.log(' пересчёт'); }
return cached;
};
}
const prices = signal([100, 200]);
const total = computed(() => prices().reduce((a,b)=>a+b,0), [prices]);
console.log('total:', total()); // пересчёт + 300
console.log('total:', total()); // мемо: без пересчёта -> 300
prices.set([100, 200, 50]);
console.log('total:', total()); // пересчёт + 350
Частые ошибки
- Пытаться записать computed. У него нет
set— он только читается. - Класть в computed побочные эффекты. Формула должна только вычислять; для эффектов есть
effect. - Хранить выводимое в обычном сигнале и руками синхронизировать — это источник рассинхрона.
Best practices
- Всё, что выводится из другого состояния, делайте через
computed, а не дублируйте. - Держите формулы чистыми — без запросов в сеть и логирования.
- Стройте цепочки computed: это читаемо и пересчитывается ровно по необходимости.
Итоги. computed выводит значение из других сигналов, пересчитываясь лениво и с мемоизацией. Он read-only и не должен иметь побочных эффектов. Для побочных эффектов есть effect — про него дальше.
Закрепляем
Золотое правило: не храните то, что можно вывести. Сумма корзины, полное имя, флаг «есть ли ошибки» — это не данные, а формулы от других данных. Для таких производных значений создан computed(): он описывает формулу, автоматически отслеживает, от каких сигналов зависит, и пересчитывается, только когда хотя бы один из источников изменился. Это избавляет от целого класса багов рассинхрона, когда вы храните копию данных и забываете её обновить.
Два свойства computed стоит запомнить накрепко. Он ленив: формула не выполняется, пока кто-нибудь не прочитает значение — нет читателей, нет и работы. И он мемоизирован: результат кешируется и пересчитывается лишь при изменении зависимостей, поэтому многократное чтение дёшево. Из этого следует и ограничение: computed обязан быть чистым — только вычислять значение, без запросов в сеть, логирования и прочих побочных эффектов. Для эффектов есть отдельный инструмент, и смешивать их роли — верный путь к трудноуловимым ошибкам.
| Свойство | Что означает |
|---|---|
| Производный | Выводится из других сигналов |
| Ленивый | Считается только при чтении |
| Мемоизированный | Кеширует, пересчёт по зависимостям |
| Read-only | Нельзя set, только чтение |