От автора: если вы похожи на меня и хотите получить всестороннее понимание механизма, как происходит в Angular 2 отслеживание изменений, вам в основном нужно исследовать источники, поскольку в Интернете мало информации.
В большинстве статей упоминается, что каждый компонент имеет свой собственный детектор изменений, который отвечает за проверку компонента, но они не выходят за рамки этого и в основном фокусируются на вариантах использования для неизменяемости и стратегии обнаружения изменений.
В этой статье вы найдете информацию, необходимую для понимания того, почему используются случаи с непременными действиями, и как стратегия обнаружения изменений влияет на проверку. Кроме того, то, что вы узнаете из этой статьи, позволит вам придумать различные сценарии для оптимизации производительности самостоятельно.
Первая часть этой статьи довольно техническая и содержит много ссылок на источники. В ней подробно объясняется, как механизм обнаружения изменений работает внутри. Его содержание основано на новейшей версии Angular (4.0.1 на момент написания). Способ, которым механизм обнаружения изменений реализован под капотом в этой версии, отличается от предыдущего 2.4.1. Если вам интересно, вы можете немного прочитать о том, как он работал в этом ответе Stack Overflow.
Во второй половине статьи показано, как в приложении может быть использовано обнаружение изменений, а его содержимое применимо как для ранних версий 2.4.1, так и для новейших версий 4.1.1 версии Angular, поскольку публичный API не изменился.
Представление как базовая концепция
Angular приложение — это дерево компонентов. Тем не менее, под капотом, Angular использует низкоуровневую абстракцию, называемую view. Существует прямая связь между представлением и компонентом: одно представление связано с одним компонентом и наоборот. В представлении содержится ссылка на соответствующий экземпляр класса в свойстве component . Все операции — такие как проверки свойств и обновления DOM выполняются на представлениях. Следовательно, более технически правильно утверждать, что Angular — это дерево представлений, в то время как компонент можно охарактеризовать как концепцию представления более высокого уровня. Вот что вы можете прочитать о представлении в источниках:
«Представление является фундаментальным строительным блоком пользовательского интерфейса приложения. Это самая маленькая группировка элементов, которые создаются и уничтожаются вместе. Свойства элементов в представлении могут меняться, но структура (количество и порядок) элементов в представлении не меняется. Изменение структуры элементов можно выполнить только путем вставки, перемещения или удаления вложенных представлений через ViewContainerRef. Каждый вид может содержать много контейнеров View.»
В этой статье я буду использовать понятия представления компонентов и компонентов взаимозаменяемо.
Здесь важно отметить, что все статьи в Интернете и ответы на Stack Overflow относительно обнаружения изменений относятся к представлению, которое я описываю здесь как объект обнаружения изменений или ChangeDetectorRef. На самом деле, нет никакого отдельного объекта для обнаружения изменений, и View — это то, что происходит при обнаружении изменений.
Каждый вид имеет ссылку на его дочерние представления через свойство node и, следовательно, может выполнять действия над дочерними представлениями.
Состояние представления
Каждое представление имеет состояние, которое играет очень важную роль, потому что, основываясь на его значении, Angular решает, следует ли запускать обнаружение изменений для представления и всех его дочерних элементов или пропустить его. Существует много возможных состояний, но следующие релевантны в контексте этой статьи:
FirstCheck
ChecksEnabled
Ошибочное
Уничтоженное
Обнаружение изменений пропускается для представления и его дочерних представлений, если ChecksEnabled задан в false или представление находится в состоянии Errored или Destroyed. По умолчанию все представления инициализируются с помощью ChecksEnabled, если не используется ChangeDetectionStrategy.OnPush . Об этом позже. Состояния могут быть объединены: например, представление может иметь оба флага FirstCheck и ChecksEnabled.
Angular имеет кучу концепций высокого уровня для управления представлениями. Я написал о некоторых из них здесь. Одной из таких концепций является ViewRef. Он инкапсулирует представление базового компонента и имеет точно названный метод detectChanges. Когда происходит асинхронное событие, Angular запускает обнаружение изменений на его самом верхнем ViewRef, который после запуска обнаружения изменений сам запускает обнаружение изменений для своих дочерних представлений.
Этот viewRef — это то, что вы можете вводить в конструктор компонента с помощью токена ChangeDetectorRef:
1 2 |
export class AppComponent { constructor(cd: ChangeDetectorRef) { ... } |
Это видно из определения класса:
1 2 3 4 5 6 7 8 9 10 11 |
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 и AfterContentChecked жизненного цикла на экземпляре дочернего компонента (AfterContentInit вызывается только во время первой проверки)
обновляет интерполяции DOM для текущего представления, если свойства экземпляра компонента текущего представления изменены
запускает обнаружение изменений для дочернего представления (повторяет шаги в этом списке)
обновляет ViewChildren запросов ViewChildren в экземпляре текущего экземпляра представления
вызывает AfterViewInit и AfterViewChecked жизненного цикла на экземпляре дочерних компонентов (AfterViewInit вызывается только во время первой проверки)
отключает проверки текущего представления (часть реализации стратегии обнаружения изменений)
Есть несколько вещей, которые следует выделить на основе перечисленных выше операций.
Во-первых, хук жизненного цикла onChanges запускается на дочернем компоненте перед проверкой дочернего представления, и он будет запущен, даже если изменение обнаружения для дочернего представления будет пропущено. Это важная информация, и мы увидим, как мы можем использовать это знание во второй части статьи.
Во-вторых, DOM для представления обновляется как часть механизма обнаружения изменений во время проверки представления. Это означает, что если компонент не проверен, DOM не обновляется, даже если свойства компонента, используемые в шаблоне, изменяются. Шаблоны отображаются до первой проверки. То, что я называю обновлением DOM, на самом деле является интерполяционным обновлением. Поэтому, если у вас есть <span>some {{name}}</span>, диапазон элементов DOM будет отображаться до первой проверки. Во время проверки будет отображаться только часть {{name}}.
Еще одно интересное наблюдение заключается в том, что состояние представления дочернего компонента может быть изменено во время обнаружения изменений. Ранее я упоминал, что все представления компонентов инициализируются с помощью ChecksEnabled по умолчанию, но для всех компонентов, использующих стратегию OnPush, изменение обнаружения отключается после первой проверки (операция 9 в списке):
1 2 3 |
if (view.def.flags & ViewFlags._OnPush_) { view.state &= ~ViewState._ChecksEnabled_; } |
Это означает, что во время следующего запуска обнаружения изменений проверка будет пропущена для этого представления компонента и всех его дочерних элементов. В документации по стратегии OnPush указано, что компонент будет проверяться, только если его привязки изменились. Чтобы сделать это, проверки необходимо активировать, установив бит ChecksEnabled. И вот что делает следующий код (операция 2):
1 2 3 |
if (compView.def.flags & ViewFlags._OnPush_) { compView.state |= ViewState._ChecksEnabled_; } |
Состояние обновляется только в том случае, если привязки родительского представления были изменены, а представление дочерних компонентов было инициализировано с помощью ChangeDetectionStrategy.OnPush.
Наконец, обнаружение изменений для текущего представления отвечает за запуск обнаружения изменений для дочерних представлений (операция 8). Это место, где проверяется состояние представления дочернего компонента, и если это ChecksEnabled, то для этого представления выполняется обнаружение изменения. Вот соответствующий код:
1 2 3 4 5 6 7 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, вот порядок перехватов вызовов и привязок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 документы определяют следующий открытый интерфейс:
1 2 3 4 5 6 7 8 |
class ChangeDetectorRef { markForCheck() : void detach() : void reattach() : void detectChanges() : void checkNoChanges() : void } |
Давайте посмотрим, как мы можем пресекать это в нашу пользу.
detach
Первый метод, который позволяет нам управлять состоянием, отключается, что просто отключает проверки текущего представления:
1 |
detach(): void { this._view.state &= ~ViewState._ChecksEnabled_; } |
Посмотрим, как это можно использовать в коде:
1 2 3 4 |
export class AComponent { constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } |
Это гарантирует, что, пока выполняется следующее обнаружение изменений, левая ветвь, начинающаяся с AComponent, будет пропущена (оранжевые компоненты не будут проверяться):
Здесь есть две вещи. Во-первых, даже если мы изменили состояние для AComponent, все его дочерние компоненты также не будут проверены. Во-вторых, поскольку для левых компонент ветвления не будет обнаружено изменение, DOM в их шаблонах также не будет обновляться. Вот небольшой пример, чтобы продемонстрировать это:
1 2 3 4 5 6 7 8 9 10 11 12 |
@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(), все будет работать так, как ожидалось.
Reattach
Как показано в первой части статьи, хук жизненного цикла OnChanges будет по-прежнему запускаться для AComponent, если входная привязка aProp изменяется на AppComponent. Это означает, что после того, как мы уведомим об изменении входных свойств, мы можем активировать детектор изменений для текущего компонента для запуска обнаружения изменений и отсоединить его от следующего тика. Вот фрагмент, демонстрирующий, что:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.reattach(); setTimeout(() => { this.cd.detach(); }) } |
Это связано с тем, что reattach просто устанавливает бит ViewState.ChecksEnabled:
1 |
reattach(): void { this._view.state |= ViewState.ChecksEnabled; } |
Это почти эквивалентно тому, что сделано, когда для параметра ChangeDetectionStrategy установлено значение OnPush: он отключает проверку после первого запуска обнаружения изменений и включает ее при изменении свойства привязки родительского компонента и отключается после запуска.
Обратите внимание, что хук OnChanges запускается только для самого верхнего компонента в отключенной ветке, а не для каждого компонента в отключенной ветке.
markForCheck
Метод reattach позволяет проверять только текущий компонент, но если обнаружение изменения не включено для его родительского компонента, оно не будет иметь никакого эффекта. Это означает, что метод reattach полезен только для самого верхнего компонента в отключенной ветке.
Нам нужен способ включить проверку всех родительских компонентов до корневого компонента. И есть метод для него — markForCheck:
1 2 3 4 5 6 7 |
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 проверяет только ссылки на объекты, мы можем выполнить грязную проверку некоторого свойства объекта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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. Этот метод запускает обнаружение изменений для текущего представления компонента независимо от его состояния, что означает, что проверки могут оставаться отключенными для текущего представления, и компонент не будет проверяться во время следующих регулярных прогонов обнаружения изменений. Вот пример:
1 2 3 4 5 6 7 8 9 10 |
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); } |
DOM обновляется при изменении свойства ввода, даже если ссылка на детектор изменений остается отсоединенной.
checkNoChanges
Этот последний метод, доступный на детекторе изменений, гарантирует, что в текущем запуске обнаружения изменений не будет сделано никаких изменений. В принципе, он выполняет операции 1,7 и 8 из вышеприведенного списка и генерирует исключение, если обнаруживает измененную привязку или определяет, что DOM должен быть обновлен.
Автор: Maximus Koretskyi
Источник: //www.sitepoint.com/
Редакция: Команда webformyself.