Компоненты и шаблоны — Примеры

Пример 1 — Массив, изменённый на месте при OnPush

Компонент списка объявляет OnPush, а родитель правит массив вместо того, чтобы заменить его. Новые строки так и не появляются. Скажите, что сравнивает стратегия, и исправьте родителя.

@Component({
  selector: "app-orders",
  template: `<li *ngFor="let order of orders">{{ order.id }}</li>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrdersComponent {
  @Input() orders: Order[] = [];
}
// родитель
addOrder(order: Order) {
  this.orders.push(order);   // та же ссылка на массив; представление не обновляется
}

Решение

// родитель
addOrder(order: Order) {
  this.orders = [...this.orders, order];   // новая ссылка; входы изменились
}

OnPush велит фреймворку проверять привязки компонента только тогда, когда изменилась ссылка на вход или внутри компонента сработало событие, и это большая победа по производительности, однако бесплатной настройкой она не даётся. Добавление в тот же массив оставляет ссылку прежней, поэтому проверка пропускается и шаблон сохраняет прежний вывод: баг читается как «данные неверны», тогда как данные в порядке, а промахнулось сравнение. Замена массива удовлетворяет сравнению ссылок и делает изменение видимым для всего остального, что сравнивает тот же вход. Урок один и тот же везде, где фреймворк сравнивает ссылки: изменишь состояние, которое собирался заменить, — и представление перестанет следовать за данными.

Пример 2 — Тяжёлое вычисление внутри привязки

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

<ul>
  <li *ngFor="let order of sortedOrders(); trackBy: trackById">
    {{ formatTotal(order) }}
  </li>
</ul>

Решение

readonly sorted = [...this.orders].sort((a, b) => b.placedAt - a.placedAt);
readonly totals = new Map(this.orders.map((o) => [o.id, formatTotal(o)]));

trackById(_: number, order: Order) { return order.id; }
<ul>
  <li *ngFor="let order of sorted; trackBy: trackById">
    {{ totals.get(order.id) }}
  </li>
</ul>

Вызов метода в привязке вычисляется на каждом проходе обнаружения изменений, а компонент со стратегией по умолчанию проверяется всякий раз, когда хоть что-то в приложении могло измениться. Поэтому сортировка внутри шаблона выполняется много раз за одно взаимодействие, а её цена невидима в коде компонента. Вычисление значения один раз в поле или перенос его в чистый пайп сводят это к одному расчёту, а чтение из карты делает поиск по строке константным. trackBy — вторая половина повторяемого списка: без него фреймворк пересоздаёт все строки при любом изменении, а это выбрасывает узлы DOM и их состояние вместо повторного использования по идентичности.