Что такое Change Detection в Angular и как она работает?

Change Detection (обнаружение изменений) — это механизм в Angular, обеспечивающий синхронизацию данных между компонентами и представлением (DOM). Он отвечает за то, чтобы при изменении данных в компоненте соответствующие изменения отображались в шаблоне и наоборот.

Основные принципы работы Change Detection

Angular использует унифицированную стратегию обнаружения изменений — zone-based change detection, основанную на библиотеке zone.js. Она патчит (перехватывает) асинхронные события (например, setTimeout, Promise, XHR и т. д.) и вызывает процесс проверки компонентов после любого потенциально изменяющего состояния действия.

Механизм проходит по дереву компонентов и сравнивает текущие значения привязанных данных с их предыдущими значениями. Если обнаружены отличия, Angular обновляет DOM.

Основные этапы цикла обнаружения изменений

  1. Срабатывает асинхронное событие (например, клик, HttpClient, setTimeout, Promise).

  2. zone.js перехватывает событие и инициирует цикл обнаружения изменений.

  3. Angular проходит дерево компонентов от корня к листьям.

  4. Для каждого компонента:

    • Выполняется метод ngDoCheck() (если реализован).

    • Вызываются все выражения в шаблоне (например, {{ value }}, [attr], *ngIf).

    • Angular сравнивает новые и старые значения.

    • Если значения изменились — обновляется DOM.

Change Detection Tree

Angular строит дерево компонентов и вызывает Change Detection рекурсивно от корневого компонента AppComponent вниз по иерархии. Каждый компонент — это узел в дереве.

Пример:

AppComponent
├── HeaderComponent
├── SidebarComponent
└── ContentComponent
├── PostComponent
└── CommentComponent

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

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

Angular поддерживает две стратегии Change Detection:

1. Default

  • Проверяются все компоненты в дереве, начиная от корня.

  • Используется по умолчанию.

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

@Component({
changeDetection: ChangeDetectionStrategy.Default
})

2. OnPush

  • Angular проверяет компонент только в двух случаях:

    • Изменился @Input(), переданный в компонент (по ссылке).

    • Вручную вызван ChangeDetectorRef.markForCheck().

  • Используется для оптимизации производительности.

  • Требует иммутабельных данных или ручного контроля.

@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})

Пример работы Default стратегии

@Component({
selector: 'app-counter',
template: \`<button (click)="increment()">{{ count }}</button>\`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}
  • При клике на кнопку zone.js перехватывает событие.

  • Angular запускает Change Detection.

  • Все компоненты в дереве проверяются, включая CounterComponent.

Пример с OnPush

@Component({
selector: 'app-item',
changeDetection: ChangeDetectionStrategy.OnPush,
template: \`{{ item.name }}\`
})
export class ItemComponent {
@Input() item!: { name: string };
}
  • Если в родительском компоненте сделать this.item.name = 'Новое имя' — Angular не отобразит изменения.

  • Нужно заменить весь объект: this.item = { name: 'Новое имя' }, чтобы Angular увидел новое значение по ссылке.

ChangeDetectorRef

Сервис ChangeDetectorRef предоставляет доступ к управлению Change Detection вручную.

Основные методы:

  • markForCheck() — помечает компонент и его родителей как нуждающиеся в проверке.

  • detectChanges() — запускает Change Detection для текущего компонента и его детей.

  • detach() — отключает компонент от дерева изменений (больше не обновляется автоматически).

  • reattach() — подключает обратно.

constructor(private cd: ChangeDetectorRef) {}
ngAfterViewInit() {
setTimeout(() => {
this.cd.detectChanges(); // вручную запускаем CD
});
}

Примеры ситуаций, когда Angular НЕ обнаружит изменения

  1. Изменение объекта по ссылке без его пересоздания:
this.obj.value = 42; // Angular не увидит это в OnPush

Решение:

this.obj = { ...this.obj, value: 42 };
  1. Мутация массивов:
this.arr.push('новый элемент'); // не работает
this.arr = \[...this.arr, 'новый элемент'\]; // работает

Примеры применения detach()

ngOnInit() {
this.cd.detach(); // отключить CD
}
updateManually() {
this.data = this.service.getData();
this.cd.detectChanges(); // вручную обновить
}

Используется для оптимизации при дорогих вычислениях или большом объёме DOM.

Особенности работы с асинхронными данными

При использовании async пайпа Angular сам подписывается на Observable/Promise и вызывает markForCheck() при получении новых значений. Это делает работу с OnPush проще:

<div \*ngIf="user$ | async as user">
{{ user.name }}
</div>

Change Detection в связке с ngZone

Можно полностью выйти из зоны Angular с помощью NgZone.runOutsideAngular() — это отключит запуск Change Detection, пока мы не вернёмся в зону:

constructor(private zone: NgZone) {}
runHeavyTask() {
this.zone.runOutsideAngular(() => {
// долгий процесс
doSomething();
// вернуть в Angular-зону и обновить
this.zone.run(() => {
this.cd.detectChanges();
});
});
}

Общая картина:

Событие Запускает CD? Angular обновит DOM?
(click) Да Да
--- --- ---
setTimeout() Да Да
--- --- ---
Observable.subscribe() Да (если в зоне) Да
--- --- ---
@Input() обновился Да Да
--- --- ---
Объект мутирован по ссылке Нет (в OnPush) Нет
--- --- ---
Новый объект присвоен Да (в OnPush) Да
--- --- ---

Change Detection — это центральный механизм реактивного обновления DOM в Angular. Он обеспечивает корректное отображение данных, но требует понимания принципов работы для оптимизации производительности, особенно при использовании стратегии OnPush. Управление изменениями вручную с помощью ChangeDetectorRef и NgZone позволяет разрабатывать высокоэффективные и масштабируемые приложения.