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

Что такое обнаружение изменений?

Основной принцип обнаружения изменений — это сравнение двух состояний: текущего и нового. Если между ними есть различия, это означает, что произошло изменение, и представление необходимо обновить.

Обнаружение изменений в Angular

В целом, процесс обнаружения изменений в Angular состоит из двух этапов: — Разработчик обновляет модель данных — Angular повторно рендерит обновлённую модель данных во вьюху

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

Посмотрим на следующую схему:

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

На изображении выше видно, что после того как механизм обнаружения изменений запускается в ForgotPasswordComponent, он проходит через все компоненты в дереве компонентов. Поток выполнения выглядит следующим образом: — Каждый компонент в дереве проверяется на предмет изменений в модели, от которой он зависит. Обнаружение изменений начинается сверху вниз. — Если Angular обнаруживает, что в другой модели также произошли изменения, он обновляет соответствующий компонент и повторно рендерит его представление.

То, как Angular запускает обнаружение изменений — начиная с корня и последовательно спускаясь вниз по дереву, — делает систему более предсказуемой и производительной. Это становится возможным благодаря реализованному в Angular однонаправленному потоку данных. Это означает, что представление не может быть обновлено после его первоначального формирования. Чтобы отобразить обновлённые изменения, Angular должен заново пройти весь цикл обнаружения изменений.

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

Стратегии обнаружения изменений в Angular

Фреймворк Angular предлагает две стратегии для выполнения обнаружения изменений:

  1. Default
  2. OnPush

Стратегия Default (по умолчанию)

Если явно не указать, какую стратегию использовать, Angular применит стратегию ChangeDetectionStrategy.Default. При такой стратегии Angular не делает предположений о зависимостях компонента и проверяет каждый компонент в дереве — от корня до листьев — при каждом событии, которое запускает механизм обнаружения изменений. К таким событиям относятся: — действия пользователя в браузере (например, клики, ввод текста), — таймеры (setTimeout, setInterval), — сетевые запросы (XHR), — асинхронные операции (Promises и др.).

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

Стратегия обнаружения изменений OnPush

Мы можем изменить стратегию обнаружения изменений с default на ChangeDetectionStrategy.OnPush, добавив свойство changeDetection в декоратор @Component.

@Component({
  selector: 'user-view',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: ...
})
export class UserView {
  ...
}

При использовании стратегии OnPush Angular пропускает проверку компонентов, у которых установлена эта стратегия, а также всех их дочерних компонентов.

При этой стратегии Angular обновляет компонент только если выполняется одно или несколько из следующих условий:

— Изменилась ссылка на входной параметр

— Произошло событие в самом компоненте или в одном из его дочерних компонентов

— AsyncPipe, связанный с шаблоном, эмитирует новое значение

— Обнаружение изменений было запущено вручную

Далее мы рассмотрим эти случаи подробнее.

Ссылка на входной параметр (Input Reference)

По умолчанию Angular запускает механизм обнаружения изменений каждый раз, когда данные, передаваемые через @Input(), изменяются или модифицируются. Однако при использовании стратегии OnPush обнаружение изменений срабатывает только если в @Input() передана новая ссылка на объект.

Именно поэтому предпочтительнее использовать иммутабельные объекты — такие объекты можно изменить только путём создания новой ссылки. Комбинируя иммутабельность и стратегию OnPush, мы получаем гарантию, что Angular отреагирует на каждое изменение, поскольку ссылка будет отличаться от предыдущей.

Срабатывание обработчика событий

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

Однако следует помнить: некоторые действия не запускают механизм обнаружения изменений при стратегии OnPush. К ним относятся: — setTimeout — setInterval — Promises — любые подписки на RxJS Observable (.subscribe())

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

AsyncPipe

Встроенный в Angular AsyncPipe внутренне вызывает markForCheck каждый раз, когда эмитируется новое значение. Позже мы рассмотрим, как вручную запускать обнаружение изменений. Исходный код можно посмотреть здесь. Благодаря тому, как реализован AsyncPipe, он работает автоматически со стратегией обнаружения изменений OnPush.

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

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

Реализация встроенного AsyncPipe в Angular

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

Ручной запуск обнаружения изменений

Мы можем вручную запустить механизм обнаружения изменений, используя методы detectChanges(), ApplicationRef.tick() и markForCheck(), который уже упоминался в контексте AsyncPipe.

— detectChanges() из ChangeDetectorRef — запускает обнаружение изменений для текущего представления и всех его потомков. — ApplicationRef.tick() — запускает обнаружение изменений во всех компонентах дерева (по сути, во всём приложении). — markForCheck() из ChangeDetectorRef — помечает всех родительских компонентов со стратегией OnPush для проверки в текущем или следующем цикле обнаружения изменений. Этот метод не запускает обнаружение изменений напрямую, а лишь указывает Angular, что соответствующие компоненты нужно проверить.

Итоги

Механизм обнаружения изменений в Angular достаточно гибкий и предоставляет нам два варианта на выбор — стратегию по умолчанию или стратегию OnPush.

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

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