Как работает механизм Change Detection в Angular и из чего он состоит?
Что проверяют
Понимаете ли вы, кто запускает проверку, как Angular обходит дерево компонентов и почему OnPush и сигналы экономят работу. Следом обычно спрашивают про ExpressionChangedAfterItHasBeenCheckedError и про zoneless-режим.
Как отвечать
Механизм складывается из трёх частей:
- триггер: классически это
zone.js. Он патчит асинхронные API (setTimeout,Promise,addEventListener, XHR), и когда очередь микрозадач в зоне Angular пустеет,NgZoneвызываетApplicationRef.tick(); - обход:
tick()идёт по дереву представлений сверху вниз и для каждого компонента сравнивает текущие значения привязок шаблона с прошлыми. Изменилось, значит обновляется DOM. Поток данных однонаправленный, от родителя к детям; - управление: у каждого компонента есть
ChangeDetectorRefс методамиmarkForCheck(),detectChanges(),detach()иreattach().
Стратегия по умолчанию проверяет всё поддерево на каждом тике. ChangeDetectionStrategy.OnPush пропускает компонент, пока его не пометили: новая ссылка во входном параметре, событие из его шаблона, значение от async pipe, изменение сигнала, прочитанного в шаблоне, или ручной markForCheck().
Сигналы меняют картину сильнее всего. Шаблон, который читает signal() или computed(), подписывается на него, и изменение помечает конкретное представление, а не всё приложение. Поэтому zoneless-режим стал стабильным, и в новых версиях свежий проект создаётся без zone.js: проверку запускают сигналы, события шаблона и markForCheck().
В режиме разработки Angular делает второй проход и бросает ExpressionChangedAfterItHasBeenCheckedError, если значение изменилось между проходами: так ловится нарушение однонаправленного потока.
Где спотыкаются
Мутируют массив или объект в OnPush-компоненте (items.push(x)) и удивляются, что экран не обновился. Ссылка та же, проверки не будет.
Лечат эту ошибку через setTimeout или detectChanges(), вместо того чтобы найти, кто меняет состояние родителя во время проверки ребёнка.
Вызывают тяжёлые функции прямо в шаблоне. Они выполняются на каждом проходе, лучше computed() или чистый pipe.