Как использовать стратегию обнаружения изменений в Angular
Введение
По умолчанию Angular 2+ выполняет обнаружение изменений во всех компонентах (сверху вниз) каждый раз, когда что-то меняется в вашем приложении. Изменение может произойти из-за пользовательского события или данных, полученных из сетевого запроса.
Обнаружение изменений очень эффективно, но по мере усложнения приложения и увеличения количества компонентов обнаружение изменений должно будет выполнять все больше и больше работы.
Одним из решений является использование стратегии обнаружения изменений OnPush
для определенных компонентов. Это даст указание Angular запускать обнаружение изменений для этих компонентов и их поддеревьев только тогда, когда им передаются новые ссылки, а не когда данные изменяются.
В этой статье вы узнаете об ChangeDetectionStrategy
и ChangeDetectorRef
.
Предпосылки
Если вы хотите следовать этой статье, вам понадобятся:
- Некоторое знакомство с компонентами Angular может оказаться полезным.
- В этой статье также упоминается библиотека RxJS, и знакомство с
BehaviorSubject
иObservable
также может быть полезным.
Изучение примера ChangeDetectionStrategy
Давайте рассмотрим образец компонента с дочерним компонентом, который отображает список водных существ и позволяет пользователям добавлять в список новых существ:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
aquaticCreatures = ['shark', 'dolphin', 'octopus'];
addAquaticCreature(newAquaticCreature) {
this.aquaticCreatures.push(newAquaticCreature);
}
}
И шаблон будет напоминать:
<input #inputAquaticCreature type="text" placeholder="Enter a new creature">
<button (click)="addAquaticCreature(inputAquaticCreature.value)">Add creature</button>
<app-child [data]="aquaticCreatures"></app-child>
Компонент app-child
будет выглядеть так:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html'
})
export class ChildComponent {
@Input() data: string[];
}
И шаблон app-child
будет выглядеть так:
<ul>
<li *ngFor="let item of data">{{ item }}</li>
</ul>
После компиляции и посещения приложения в браузере вы должны увидеть неупорядоченный список, содержащий акулу
, дельфина
и осьминога
.
Введите водное существо в поле ввода и нажмите кнопку «Добавить существо», чтобы добавить новое существо в список.
Дочерний компонент обновляется, когда Angular обнаруживает, что данные изменились в родительском компоненте.
Теперь давайте установим стратегию обнаружения изменений в дочернем компоненте на OnPush
:
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() data: string[];
}
После перекомпиляции и просмотра приложения в браузере вы должны увидеть несортированный список, содержащий акулу
, дельфина
и осьминога
.
Однако добавление нового водного существа, похоже, не добавляет его в неупорядоченный список. Новые данные по-прежнему помещаются в массив aquaticCreatures
в родительском компоненте, но Angular не распознает новую ссылку для ввода данных и, следовательно, не выполняет обнаружение изменений в компоненте.
Чтобы передать новую ссылку на ввод данных, вы можете заменить Array.push
на синтаксис расширения (...)
в addAquaticCreature
:
// ...
addAquaticCreature(newAquaticCreature) {
this.aquaticCreatures = [...this.aquaticCreatures, newAquaticCreature];
}
// ...
В этом варианте вы больше не изменяете массив aquaticCreatures
. Вы возвращаете совершенно новый массив.
После перекомпиляции вы должны заметить, что приложение ведет себя как прежде. Angular обнаружил новую ссылку на data
, поэтому запустил обнаружение изменений в дочернем компоненте.
На этом завершается изменение образца родительского и дочернего компонентов для использования стратегии обнаружения изменений OnPush
.
Изучение примеров ChangeDetectorRef
При использовании стратегии обнаружения изменений OnPush
, помимо обязательной передачи новых ссылок каждый раз, когда что-то должно измениться, вы также можете использовать ChangeDetectorRef
для полного контроля.
ChangeDetectorRef.detectChanges()
Например, вы можете продолжать изменять свои данные, а затем иметь кнопку в дочернем компоненте с кнопкой «Обновить».
Это потребует возврата addAquaticCreature
для использования Array.push
:
// ...
addAquaticCreature(newAquaticCreature) {
this.aquaticCreatures.push(newAquaticCreature);
}
// ...
И добавьте элемент button
, который вызывает refresh()
:
<ul>
<li *ngFor="let item of data">{{ item }}</li>
</ul>
<button (click)="refresh()">Refresh</button>
Затем измените дочерний компонент, чтобы он использовал ChangeDetectorRef
:
import {
Component,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() data: string[];
constructor(private cd: ChangeDetectorRef) {}
refresh() {
this.cd.detectChanges();
}
}
После компиляции и посещения приложения в браузере вы должны увидеть неупорядоченный список, содержащий акулу
, дельфина
и осьминога
.
Добавление новых элементов в массив не обновляет неупорядоченный список. Однако нажатие кнопки «Обновить» запустит обнаружение изменений в компоненте, и будет выполнено обновление.
ChangeDetectorRef.markForCheck()
Допустим, ваши вводимые данные на самом деле являются наблюдаемыми.
В этом примере будет использоваться RxJS BehaviorSubject
:
import { Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
aquaticCreatures = new BehaviorSubject(['shark', 'dolphin', 'octopus']);
addAcquaticCreature((newAquaticCreature) {
this.aquaticCreatures.next(newAquaticCreature);
}
}
И вы подписываетесь на него в хуке OnInit
в дочернем компоненте.
Вы добавите водных существ в массив aquaticCreatures
здесь:
import {
Component,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnInit
} from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
@Input() data: Observable<any>;
aquaticCreatures: string[] = [];
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
<^>this.data.subscribe(newAquaticCreature => {
this.aquaticCreatures = [...this.aquaticCreatures, ...newAquaticCreature];
});
}
}
Этот код не завершен, потому что новые данные изменяют наблюдаемый data
, поэтому Angular не запускает обнаружение изменений. Решение состоит в том, чтобы вызвать ChangeDetectorRef
markForCheck
, когда вы подписываетесь на наблюдаемое:
// ...
ngOnInit() {
this.data.subscribe(newAquaticCreature => {
this.aquaticCreatures = [...this.aquaticCreatures, ...newAquaticCreature];
this.cd.markForCheck();
});
}
// ...
markForCheck
указывает Angular, что этот конкретный ввод должен инициировать обнаружение изменений при мутации.
ChangeDetectorRef.detach() и ChangeDetectorRef.reattach()
Еще одна мощная вещь, которую вы можете сделать с помощью ChangeDetectorRef
, — полностью отключить и снова подключить обнаружение изменений вручную с помощью методов detach
и reattach
.
Заключение
В этой статье вы познакомились с ChangeDetectionStrategy
и ChangeDetectorRef
. По умолчанию Angular будет выполнять обнаружение изменений для всех компонентов. ChangeDetectionStrategy
и ChangeDetectorRef
можно применять к компонентам для обнаружения изменений в новых ссылках, а не при изменении данных.
Если вы хотите узнать больше об Angular, ознакомьтесь с нашей темой по Angular, где вы найдете упражнения и проекты по программированию.