Все, что вам нужно знать о change detection в Angular
Если вы, как и я, хотите иметь полное представление о механизме обнаружения изменений в Angular, вам придется изучить источники, так как в Интернете не так много информации. В большинстве статей упоминается, что у каждого компонента есть свой собственный детектор изменений, который отвечает за проверку компонента, но они не выходят за рамки этого и в основном фокусируются на сценариях использования неизменяемых объектов и стратегии обнаружения изменений. Эта статья предоставляет вам информацию, необходимую для понимания, почему работают варианты использования с неизменяемыми значениями и как стратегия обнаружения изменений влияет на проверку. Кроме того, из того, что вы узнаете из этой статьи, вы сможете самостоятельно разрабатывать различные сценарии оптимизации производительности.
Эта статья состоит из двух частей. Первая часть довольно техническая и содержит много ссылок на источники. Подробно объясняется, как механизм обнаружения изменений работает под капотом.
Во второй части показано, как обнаружение изменений может использоваться в приложении, и его содержимое применимо как для более ранних 2.4.1, так и для новейших версий Angular 4.0.1, поскольку открытый API не изменился.
Рассматривать как основную концепцию
В руководствах упоминалось, что приложение Angular - это дерево компонентов. Тем не менее, под капотом Angular использует низкоуровневую абстракцию под названием view. Существует прямая связь между представлением и компонентом - одно представление связано с одним компонентом, и наоборот. Представление содержит ссылку на связанный экземпляр класса компонента в свойстве component
. Все операции, такие как проверка свойств и обновления DOM, выполняются для представлений, поэтому более технически правильно утверждать, что angular является деревом представлений, тогда как компонент может быть описан как концепция представления более высокого уровня. Вот что вы можете прочитать о представлении в источниках:
Представление является фундаментальным строительным блоком пользовательского интерфейса приложения. Это самая маленькая группа элементов, которые создаются и уничтожаются вместе.
Свойства элементов в представлении могут изменяться, но структура (количество и порядок) элементов в представлении не может. Изменить структуру элементов можно только путем вставки, перемещения или удаления вложенных представлений через ViewContainerRef. Каждое представление может содержать много контейнеров просмотра.
В этой статье я буду использовать понятия компонентного представления и взаимозаменяемого компонента.
Здесь важно отметить, что все статьи в Интернете и ответы на StackOverflow, касающиеся обнаружения изменений, относятся к представлению, которое я описываю здесь как объект детектора изменений или ChangeDetectorRef. В действительности нет отдельного объекта для обнаружения изменений, а View - это то, на чем работает обнаружение изменений.
Каждое представление имеет ссылку на свои дочерние представления через свойство узлов и, следовательно, может выполнять действия с дочерними представлениями.
Просмотр состояния
Каждое представление имеет состояние, которое играет очень важную роль, поскольку на основе его значения Angular решает, следует ли запустить обнаружение изменений для представления и всех его дочерних элементов или пропустить его. Существует множество возможных состояний, но в контексте этой статьи актуальны следующие:
- FirstCheck
- ChecksEnabled
- Errored
- Destroyed
Обнаружение изменений пропускается для представления и его дочерних представлений, если ChecksEnabled
имеет значение false
или представление находится в состоянии Errored
или Destroyed
. По умолчанию все представления инициализируются с помощью, ChecksEnabled
если ChangeDetectionStrategy.OnPush
не используется. Подробнее об этом позже. Состояния могут быть объединены, например, представление может иметь оба флага FirstCheck
и ChecksEnabled
.
В Angular есть куча концепций высокого уровня для манипулирования представлением. Я написал о некоторых из них здесь . Одним из таких понятий является ViewRef. Он инкапсулирует представление базового компонента и имеет метко названный метод detectChanges. Когда происходит асинхронное событие, Angular запускает обнаружение изменений в своем самом верхнем ViewRef, который после запуска обнаружения изменений сам запускает обнаружение изменений для своих дочерних представлений .
Вот пример viewRef
который вы можете вставить в конструктор компонента, используя токен ChangeDetectorRef
:
export class AppComponent { constructor(cd: ChangeDetectorRef) { ... }
Как видно из этого определения классов:
export declare abstract class ChangeDetectorRef { abstract checkNoChanges(): void; abstract detach(): void; abstract detectChanges(): void; abstract markForCheck(): void; abstract reattach(): void; } export abstract class ViewRef extends ChangeDetectorRef { ... }
Операции обнаружения изменений
Основная логика, отвечающая за запуск обнаружения изменений для представления, находится в функции checkAndUpdateView. Большая часть его функций выполняет операции с представлениями дочерних компонентов. Эта функция вызывается рекурсивно для каждого компонента, начиная с хост-компонента. Это означает, что дочерний компонент становится родительским компонентом при следующем вызове, когда разворачивается рекурсивное дерево.
Когда эта функция запускается для определенного представления, она выполняет следующие операции в указанном порядке:
- устанавливает
ViewState.firstCheck
какtrue
если представление проверяется первый раз, иfalse
если он уже был проверен до этого - проверяет и обновляет входные свойства в экземпляре дочернего компонента / директивы
- обновляет состояние обнаружения изменений дочернего представления (часть реализации стратегии обнаружения изменений)
- запускает обнаружение изменений для встроенных представлений (повторяет шаги в списке)
- вызывает ловушку жизненного цикла дочернего компонента
OnChanges
, если привязки изменены - вызовы
OnInit
иngDoCheck
на дочернем компоненте (OnInit
вызывается только при первой проверке) - обновляет список запросов
ContentChildren
в экземпляре компонента дочернего представления - вызовы ловушек жизненного цикла экземпляра дочернего компонента
AfterContentInit
иAfterContentChecked
(AfterContentInit
вызывается только при первой проверке) - обновляет интерполяции DOM для текущего представления, если изменились свойства в экземпляре компонента текущего представления
- запускает обнаружение изменений для дочернего представления (повторяет шаги в этом списке)
- обновляет список запросов
ViewChildren
в текущем экземпляре компонента представления - вызовы ловушек жизненного цикла экземпляра дочернего компонента
AfterViewInit
иAfterViewChecked
(AfterViewInit
вызывается только при первой проверке) - отключает проверки текущего представления (часть реализации стратегии обнаружения изменений)
Есть несколько вещей, которые нужно выделить на основе операций, перечисленных выше.
Во-первых, onChanges
запускается на дочернем компоненте до того, как проверяется дочернее представление, и будет запускаться, даже если измененное обнаружение для дочернего представления будет пропущено. Это важная информация, и мы увидим, как мы можем использовать эти знания во второй части статьи.
Во-вторых, DOM для представления обновляется как часть механизма обнаружения изменений при проверке представления. Это означает, что если компонент не проверен, DOM не обновляется, даже если изменяются свойства компонента, используемые в шаблоне. Шаблоны отображаются перед первой проверкой. То, что я называю обновлением DOM, на самом деле является обновлением интерполяции. Так что если у вас есть some {{name}}
, элемент DOM span
будет отображаться до первой проверки. Во время проверки {{name}}
будет предоставлена только часть.
Другое интересное наблюдение состоит в том, что состояние представления дочернего компонента может быть изменено во время обнаружения изменений. Ранее я упоминал, что все представления компонентов инициализируются c ChecksEnabled
по умолчанию, но для всех компонентов, использующих OnPush
стратегии, отключается после первой проверки (операция 9 в списке):
if (view.def.flags & ViewFlags.OnPush) { view.state &= ~ViewState.ChecksEnabled; }
Это означает, что во время следующего запуска обнаружения изменений проверка будет пропущена для этого представления компонента и всех его дочерних элементов. В документации о OnPush
стратегии говорится, что компонент будет проверен, только если его привязки изменились. Поэтому для этого необходимо включить проверку, установив ChecksEnabled
. И это то, что делает следующий код (операция 2):
if (compView.def.flags & ViewFlags.OnPush) { compView.state |= ViewState.ChecksEnabled; }
Состояние обновляется только в том случае, если изменились привязки родительского представления и было инициализировано представление дочернего компонента ChangeDetectionStrategy.OnPush
.
Наконец, обнаружение изменений для текущего представления отвечает за запуск обнаружения изменений для дочерних представлений (операция 8). Это место, где проверяется состояние представления дочернего компонента, и если это так, для этого представления выполняется обнаружение изменений. Вот соответствующий код:
viewState = view.state; ... case ViewAction.CheckAndUpdate: if ((viewState & ViewState.ChecksEnabled) && (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { checkAndUpdateView(view); } }
Теперь вы знаете, что состояние представления контролирует, выполняется ли обнаружение изменений для этого представления и его дочерних элементов или нет. Таким образом, возникает вопрос - можем ли мы контролировать это состояние? Оказывается, мы можем, и это то, о чем вторая часть этой статьи.
Некоторые ловушки жизненного цикла вызываются до обновления DOM (3,4,5), а некоторые после (9). Так что, если у вас есть следующая иерархия компонентов:, A -> B -> C
вот порядок вызовов ловушек и обновлений привязок:
A: AfterContentInit A: AfterContentChecked A: Update bindings B: AfterContentInit B: AfterContentChecked B: Update bindings C: AfterContentInit C: AfterContentChecked C: Update bindings C: AfterViewInit C: AfterViewChecked B: AfterViewInit B: AfterViewChecked A: AfterViewInit A: AfterViewChecked
Изучение последствий
Давайте предположим, что у нас есть следующее дерево компонентов:
Как мы узнали выше, каждый компонент связан с представлением компонента. Каждое представление инициализируется с помощью ViewState.ChecksEnabled
, которое означает, что при обнаружении изменения будет проверяться каждый компонент в дереве.
Предположим, мы хотим отключить обнаружение изменений для AComponent
и его дочерних элементов. Это легко сделать - нам нужно просто установить ViewState.ChecksEnabled
в false
. Изменение состояния является операцией низкого уровня, поэтому Angular предоставляет нам несколько открытых методов, доступных в представлении. Каждый компонент может получить связанный вид через ChangeDetectorRef
. Для этого класса Angular Docs определяют следующий открытый интерфейс:
class ChangeDetectorRef { markForCheck() : void detach() : void reattach() : void detectChanges() : void checkNoChanges() : void }
Давайте посмотрим, что мы можем изменить в нашу пользу.
detach
Первый метод, который позволяет нам манипулировать состоянием, - detach
это просто отключение проверок текущего представления:
detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }
Посмотрим, как это можно использовать в коде:
export class AComponent { constructor(public cd: ChangeDetectorRef) { this.cd.detach(); }
Это гарантирует, что во время следующих запусков обнаружения изменений левая ветвь, начинающаяся с AComponent
будет пропущена (оранжевые компоненты не будут проверены):
Здесь следует отметить две вещи: во-первых, хотя мы изменили состояние для AComponent
, все его дочерние компоненты также не будут проверяться. Во-вторых, поскольку обнаружение изменений не будет выполняться для компонентов левой ветви, DOM в их шаблонах также не будет обновляться. Вот небольшой пример, чтобы продемонстрировать это:
@Component({
selector: 'a-comp',
template: `See if I change: {{changed}}`
})
export class AComponent {
constructor(public cd: ChangeDetectorRef) {
this.changed = 'false'; setTimeout(() => {
this.cd.detach();
this.changed = 'true';
}, 2000);
}
При первой проверке компонента будет отображаться с текстом See if I change: false
. И в течение двух секунд при обновлении свойства changed
до true
текст в промежутке не изменится. Однако, если мы удалим эту строку this.cd.detach()
, все будет работать так, как ожидается.
reattach
Как показано в первой части статьи, OnChanges
будет по-прежнему срабатывать, если AComponent
входная привязка aProp
изменяется на AppComponent
. Это означает, что как только мы получим уведомление об изменении входных свойств, мы можем активировать детектор изменений для текущего компонента, чтобы запустить обнаружение изменений и отключить его на следующем тике. Вот фрагмент, демонстрирующий, что:
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.reattach(); setTimeout(() => { this.cd.detach(); }) }
Так как reattach
просто устанавливает ViewState.ChecksEnabled
:
reattach(): void { this._view.state |= ViewState.ChecksEnabled; }
Это почти эквивалентно тому, что делается, когда ChangeDetectionStrategy
установлено значение OnPush
: отключить проверку после первого запуска обнаружения изменений, включить ее при изменении свойства привязки родительского компонента и отключить после запуска.
Обратите внимание, что
OnChanges
ловушка запускается только для самого верхнего компонента в отключенной ветви, а не для каждого компонента в данной ветви.
markForCheck
Метод reattach
позволяет проверяет наличие только текущего компонента, но если changed detection не включается для родительского компонента, он не будет иметь никакого эффекта. Это означает, что метод reattach
полезен только для самого верхнего компонента в отключенной ветке.
Нам нужен способ включить проверку всех родительских компонентов вплоть до корневого компонента. И есть способ для этого markForCheck
:
let currView: ViewData|null = view; while (currView) { if (currView.def.flags & ViewFlags.OnPush) { currView.state |= ViewState.ChecksEnabled; } currView = currView.viewContainerParent || currView.parent; }
Как видно из реализации, он просто выполняет итерацию вверх и включает проверки для каждого родительского компонента вплоть до корневого.
Когда это полезно? Так же , как и с ngOnChanges
к ngDoCheck
хук жизненного цикла срабатывают даже если компонент использует OnPush
стратегию. Опять же, он запускается только для самого верхнего компонента в отключенной ветви, а не для каждого компонента в отключенной ветви. Но мы можем использовать эту ловушку для выполнения пользовательской логики и пометить наш компонент как подходящий для одного цикла обнаружения изменений. Поскольку Angular проверяет только ссылки на объекты, мы можем реализовать грязную проверку некоторых свойств объекта:
Component({ ..., changeDetection: ChangeDetectionStrategy.OnPush }) MyComponent { @Input() items; prevLength; constructor(cd: ChangeDetectorRef) {} ngOnInit() { this.prevLength = this.items.length; } ngDoCheck() { if (this.items.length !== this.prevLength) { this.cd.markForCheck(); this.prevLenght = this.items.length; } }
detectChanges
Существует способ запустить обнаружение изменений один раз для текущего компонента и всех его дочерних элементов. Это делается с помощью метода detectChanges
. Этот метод запускает обнаружение изменений для текущего представления компонента независимо от его состояния, что означает, что проверки могут оставаться отключенными для текущего представления, и компонент не будет проверяться во время следующих регулярных прогонов обнаружения изменений. Вот пример:
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); }
DOM обновляется при изменении входного свойства, даже если ссылка на детектор изменений остается отсоединенной.
checkNoChanges
Этот последний метод, доступный на детекторе изменений, гарантирует, что в текущем цикле обнаружения изменений не будет внесено никаких изменений. По сути, он выполняет 1,7,8 операции из списка в первой статье и выдает исключение, если находит измененную привязку или определяет, что DOM следует обновить.
Перевод статьи: Everything you need to know about change detection in Angular