Эта статья об обнаружении изменений в Angular была первоначально опубликована в блоге Angular In Depth и публикуется здесь с разрешения.
Если вы похожи на меня и хотите получить полное представление о механизме обнаружения изменений в Angular, вам, в основном, нужно изучить источники, так как в Интернете не так много информации.
В большинстве статей упоминается, что у каждого компонента есть свой собственный детектор изменений, который отвечает за проверку компонента, но они не выходят за рамки этого и в основном фокусируются на сценариях использования неизменяемых объектов и стратегии обнаружения изменений.
Эта статья предоставляет вам информацию, необходимую для понимания, почему работают варианты использования с неизменяемыми значениями и как стратегия обнаружения изменений влияет на проверку. Кроме того, то, что вы узнаете из этой статьи, позволит вам самостоятельно разработать различные сценарии оптимизации производительности.
Первая часть этой статьи довольно техническая и содержит множество ссылок на источники. Подробно объясняется, как механизм обнаружения изменений работает под капотом. Его содержание основано на новейшей версии Angular (4.0.1 на момент написания). Способ реализации механизма обнаружения изменений в этой версии отличается от предыдущего 2.4.1. Если вам интересно, вы можете прочитать немного о том, как это работает, в этом ответе о переполнении стека .
Во второй половине статьи показано, как обнаружение изменений можно использовать в приложении, и его содержимое применимо как к более ранней версии Angular 2.4.1, так и к самым новым версиям Angular 4.0.1, поскольку открытый API не изменился.
Рассматривать как основную концепцию
Угловое приложение — это дерево компонентов. Однако под капотом Angular использует низкоуровневую абстракцию, называемую view . Между представлением и компонентом существует прямая связь: одно представление связано с одним компонентом и наоборот. Представление содержит ссылку на связанный экземпляр класса component
свойстве component
. Все операции, такие как проверка свойств и обновление DOM, выполняются в представлениях. Следовательно, технически более правильно утверждать, что Angular является деревом представлений, в то время как компонент может быть описан как представление представления более высокого уровня. Вот что вы можете прочитать о представлении в источниках :
Представление является фундаментальным строительным блоком пользовательского интерфейса приложения. Это самая маленькая группа элементов, которые создаются и уничтожаются вместе.
Свойства элементов в представлении могут изменяться, но структура (количество и порядок) элементов в представлении не могут. Изменить структуру элементов можно только путем вставки, перемещения или удаления вложенных представлений через ViewContainerRef. Каждый просмотр может содержать много контейнеров просмотра.
В этой статье я буду использовать взаимозаменяемость представлений компонентов и компонентов.
Здесь важно отметить, что все статьи в Интернете и ответы о переполнении стека, касающиеся обнаружения изменений, относятся к представлению, которое я здесь описываю, как объект детектора изменений или ChangeDetectorRef. В действительности нет отдельного объекта для обнаружения изменений, а View — это то, на чем работает обнаружение изменений.
Каждое представление имеет ссылку на свои дочерние представления через свойство узлов и, следовательно, может выполнять действия с дочерними представлениями.
Просмотр состояния
Каждое представление имеет состояние , которое играет очень важную роль, поскольку на основе своего значения Angular решает, следует ли запустить обнаружение изменений для представления и всех его дочерних элементов или пропустить его. Существует много возможных состояний , но в контексте этой статьи актуальны следующие:
- FirstCheck
- ChecksEnabled
- Errored
- разрушенный
Обнаружение изменений пропускается для представления и его дочерних представлений, если ChecksEnabled
имеет значение false
или представление находится в состоянии Errored
или Destroyed
. По умолчанию все представления инициализируются с помощью ChecksEnabled
если не используется ChangeDetectionStrategy.OnPush
. Подробнее об этом позже. Состояния могут быть объединены: например, у представления могут быть установлены оба FirstCheck
и ChecksEnabled
.
В Angular есть куча концепций высокого уровня для манипулирования видами. Я написал о некоторых из них здесь . Одним из таких понятий является ViewRef . Он инкапсулирует представление базового компонента и имеет метко названный метод detectChanges . Когда происходит асинхронное событие, Angular запускает обнаружение изменений в своем самом верхнем ViewRef, который после запуска обнаружения изменений самостоятельно запускает обнаружение изменений для своих дочерних представлений .
Этот 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
жизненного циклаOnChanges
к дочернему компоненту, если привязки изменились - вызывает
OnInit
иngDoCheck
для дочернего компонента (OnInit
вызывается только при первой проверке) - обновляет список запросов
ContentChildren
экземпляре компонента дочернего представления - вызывает
AfterContentInit
жизненного циклаAfterContentInit
иAfterContentChecked
экземпляра дочернего компонента (AfterContentInit
вызывается только во время первой проверки) - обновляет интерполяции DOM для текущего представления, если изменены свойства экземпляра компонента текущего представления
- запускает обнаружение изменений для дочернего представления (повторяет шаги в этом списке)
- обновляет
ViewChildren
запросовViewChildren
для текущего экземпляра компонента представления - вызывает
AfterViewInit
жизненного циклаAfterViewInit
иAfterViewChecked
экземпляра дочернего компонента (AfterViewInit
вызывается только во время первой проверки) - отключает проверки текущего представления (часть реализации стратегии обнаружения изменений)
Есть несколько вещей, которые нужно выделить на основе операций, перечисленных выше.
Во-первых, onChanges
жизненного цикла onChanges
запускается на дочернем компоненте до того, как проверяется дочернее представление, и будет запускаться, даже если измененное обнаружение для дочернего представления будет пропущено. Это важная информация, и мы увидим, как мы можем использовать эти знания во второй части статьи.
Во-вторых, DOM для представления обновляется как часть механизма обнаружения изменений во время проверки представления. Это означает, что если компонент не проверен, DOM не обновляется, даже если изменяются свойства компонента, используемые в шаблоне. Шаблоны отображаются перед первой проверкой. То, что я называю обновлением DOM, на самом деле является обновлением интерполяции. Так что, если у вас есть <span>some {{name}}</span>
, span
элемента DOM будет обработан перед первой проверкой. Во время проверки будет отображаться только часть {{name}}
.
Другое интересное наблюдение заключается в том, что состояние представления дочернего компонента может быть изменено во время обнаружения изменений. Ранее я упоминал, что все виды компонентов по умолчанию инициализируются с помощью 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). Это место, где проверяется состояние представления дочернего компонента, и если оно ChecksEnabled
, то для этого представления выполняется обнаружение изменений. Вот соответствующий код:
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
, что означает, что когда Angular запускает обнаружение изменений, каждый компонент в дереве будет проверен.
Предположим, мы хотим отключить обнаружение изменений для AComponent
и его дочерних AComponent
. Это легко сделать — нам просто нужно установить ViewState.ChecksEnabled
в false
. Изменение состояния является операцией низкого уровня, поэтому Angular предоставляет нам несколько открытых методов, доступных в представлении. Каждый компонент может получить связанный вид через токен ChangeDetectorRef
. Для этого класса документы Angular определяют следующий открытый интерфейс:
class ChangeDetectorRef { markForCheck() : void detach() : void reattach() : void detectChanges() : void checkNoChanges() : void }
Давайте посмотрим, как мы можем использовать это в наших интересах.
открепление
Первый метод, который позволяет нам манипулировать состоянием, это 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: `<span>See if I change: {{changed}}</span>`}) 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()
, все будет работать так, как ожидается.
Присоедините
Как показано в первой части статьи, OnChanges
жизненного цикла 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
включает проверки только для текущего компонента, но если обнаружение изменений не включено для его родительского компонента, это не будет иметь никакого эффекта. Это означает, что метод 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
жизненного цикла 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
метода detectChanges
. Этот метод запускает обнаружение изменений для текущего представления компонента независимо от его состояния, что означает, что проверки могут оставаться отключенными для текущего представления, и компонент не будет проверяться во время следующих регулярных запусков обнаружения изменений. Вот пример:
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); }
DOM обновляется при изменении свойства ввода, даже если ссылка на детектор изменений остается отсоединенной.
checkNoChanges
Этот последний метод, доступный на детекторе изменений, гарантирует, что в текущем цикле обнаружения изменений не будет внесено никаких изменений. По сути, он выполняет операции 1,7 и 8 из приведенного выше списка и выдает исключение, если он находит измененную привязку или определяет, что DOM следует обновить.