Оригинальная ссылка:A Comprehensive Guide to Angular onPush Change Detection Strategy
Оригинальный автор: Нетанел Базаль
Переводчик: Мяо Мяо
Стратегия обнаружения изменений по умолчанию
По умолчанию Angular используетChangeDetectionStrategy.Default
стратегия обнаружения изменений.
Стратегия по умолчанию не делает никаких предположений о приложении заранее, поэтому всякий раз, когда пользовательские события, таймеры, XHR, промисы и т. д. вызывают изменение данных в приложении, обнаружение изменений выполняется во всех компонентах.
Это означает, что все, от события клика до данных, полученных от вызова ajax, вызовет обнаружение изменений.
Мы можем легко увидеть это, определив геттер в компоненте и используя его в шаблоне:
@Component({
template: `
<h1>Hello {{name}}!</h1>
{{runChangeDetection}}
`
})
export class HelloComponent {
@Input() name: string;
get runChangeDetection() {
console.log('Checking the view');
return true;
}
}
@Component({
template: `
<hello></hello>
<button (click)="onClick()">Trigger change detection</button>
`
})
export class AppComponent {
onClick() {}
}
После выполнения приведенного выше кода всякий раз, когда мы нажимаем кнопку. Angular пройдет цикл обнаружения изменений, и в консоли мы увидим две строки журнала «Проверка представления».
Этот метод называется грязной проверкой. Чтобы узнать, нужно ли обновлять представление, Angular должен получить доступ к новому значению и сравнить его со старым значением, чтобы определить, нужно ли обновлять представление.
Теперь представьте, что если у нас есть большое приложение с тысячами выражений, и Angular проверяет каждое из них, мы можем столкнуться с проблемами производительности.
Итак, есть ли у нас способ активно сообщать Angular, когда проверять наши компоненты?
Стратегия обнаружения изменений OnPush
Мы можем поместить компонентChangeDetectionStrategy
установлен вChangeDetectionStrategy.OnPush
.
Это скажет Angular, что компонент зависит только от его@inputs()
, что необходимо проверять только в следующих случаях:
1. Input
цитаты изменены
установивonPush
Стратегия обнаружения изменений, наше соглашение с Angular требует использования неизменяемых объектов (или наблюдаемых объектов, которые будут представлены позже).
Преимущество использования неизменяемых объектов в контексте обнаружения изменений заключается в том, что Angular может определить, нужно ли проверять представление, проверив, изменилась ли ссылка. Это будет намного проще, чем глубокая проверка.
Давайте попробуем модифицировать объект и посмотрим на результат.
@Component({
selector: 'tooltip',
template: `
<h1>{{config.position}}</h1>
{{runChangeDetection}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent {
@Input() config;
get runChangeDetection() {
console.log('Checking the view');
return true;
}
}
@Component({
template: `
<tooltip [config]="config"></tooltip>
`
})
export class AppComponent {
config = {
position: 'top'
};
onClick() {
this.config.position = 'bottom';
}
}
На данный момент я не вижу никаких журналов, когда нажимаю кнопку, это потому, что Angular сравнивает старое значение с новой ссылкой на значение, например:
/** Returns false in our case */
if( oldValue !== newValue ) {
runChangeDetection();
}
Стоит отметить, что числа, логические значения, строки, null и undefined — все это примитивные типы. Все примитивные типы передаются по значению.Объекты, массивы и функции также передаются по значению, но значениекопия указанного адреса.
Итак, чтобы инициировать обнаружение изменений для этого компонента, нам нужно изменить ссылку на этот объект.
@Component({
template: `
<tooltip [config]="config"></tooltip>
`
})
export class AppComponent {
config = {
position: 'top'
};
onClick() {
this.config = {
position: 'bottom'
}
}
}
После изменения ссылки на объект мы увидим, что представление проверено и отображается новое значение.
2. События, происходящие из этого компонента или его подкомпонентов
Когда событие запускается в компоненте или его подкомпонентах, внутреннее состояние компонента обновляется. Например:
@Component({
template: `
<button (click)="add()">Add</button>
{{count}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
add() {
this.count++;
}
}
Когда мы нажимаем кнопку, Angular выполняет цикл обнаружения изменений и обновляет представление.
Вы можете подумать, как мы сказали в начале, что каждый асинхронный API инициирует обнаружение изменений, но это не так.
Вы обнаружите, что это правило применяется только к событиям DOM, следующие API не запускают обнаружение изменений:
@Component({
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
constructor() {
setTimeout(() => this.count = 5, 0);
setInterval(() => this.count = 5, 100);
Promise.resolve().then(() => this.count = 5);
this.http.get('https://count.com').subscribe(res => {
this.count = res;
});
}
add() {
this.count++;
}
Обратите внимание, что вы все еще обновляете свойство, поэтому в следующем процессе обнаружения изменений, например при нажатии кнопки, значение счетчика станет равным 6 (5+1).
3. Отображается для обнаружения изменений
Angular дает нам 3 способа запуска обнаружения изменений.
первыйdetectChanges()
чтобы указать Angular выполнить обнаружение изменений в этом компоненте и его дочерних компонентах.
@Component({
selector: 'counter',
template: `{{count}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
constructor(private cdr: ChangeDetectorRef) {
setTimeout(() => {
this.count = 5;
this.cdr.detectChanges();
}, 1000);
}
}
ВторойApplicationRef.tick()
, который указывает Angular выполнять обнаружение изменений во всем приложении.
tick() {
try {
this._views.forEach((view) => view.detectChanges());
...
} catch (e) {
...
}
}
ТретийmarkForCheck()
, он не запускает обнаружение изменений. Вместо этого он будет проверять все флаги предков, для которых установлен onPush в текущем или следующем цикле обнаружения изменений.
markForCheck(): void {
markParentViewsForCheck(this._view);
}
export function markParentViewsForCheck(view: ViewData) {
let currView: ViewData|null = view;
while (currView) {
if (currView.def.flags & ViewFlags.OnPush) {
currView.state |= ViewState.ChecksEnabled;
}
currView = currView.viewContainerParent || currView.parent;
}
}
Важно отметить, что ручное обнаружение изменений не является «хаком», это по замыслу Angular и является очень разумным поведением (конечно, в разумных сценариях).
🤓 Угловой асинхронный канал
async
Канал подписывается на Observable или Promise и возвращает самое последнее сгенерированное им значение.
давайте посмотрим на одинinput()
Является компонентом onPush наблюдаемого объекта.
@Component({
template: `
<button (click)="add()">Add</button>
<app-list [items$]="items$"></app-list>
`
})
export class AppComponent {
items = [];
items$ = new BehaviorSubject(this.items);
add() {
this.items.push({ title: Math.random() })
this.items$.next(this.items);
}
}
@Component({
template: `
<div *ngFor="let item of _items ; ">{{item.title}}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
@Input() items: Observable<Item>;
_items: Item[];
ngOnInit() {
this.items.subscribe(items => {
this._items = items;
});
}
}
Когда мы нажимаем кнопку, мы не видим обновление представления. Это связано с тем, что ни одна из вышеупомянутых ситуаций не произошла, поэтому Angular не проверяет компонент в текущем цикле обнаружения изменений.
Теперь добавимasync
Попробуйте трубу.
@Component({
template: `
<div *ngFor="let item of items | async">{{item.title}}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
@Input() items;
}
Теперь вы можете видеть, что когда мы нажимаем кнопку, представление также обновляется. Причина в том, что когда выдается новое значение,async
Труба помечает компонент как измененный и требует проверки. Мы видим в исходном коде:
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
Angular звонит намmarkForCheck()
, поэтому мы можем видеть, что представление обновляется, даже если ссылка ввода не изменилась.
Если компонент зависит только от своего входного свойства, а входное свойство является наблюдаемым, то компонент будет изменяться только тогда, когда его входное свойство генерирует событие.
Небольшой совет: выставлять объект на улицу не рекомендуется, всегда используйтеasObservable()
метод выявления наблюдаемого.
👀 onPush и просмотр запроса
@Component({
selector: 'app-tabs',
template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
@ContentChild(TabComponent) tab: TabComponent;
ngAfterContentInit() {
setTimeout(() => {
this.tab.content = 'Content';
}, 3000);
}
}
@Component({
selector: 'app-tab',
template: `{{content}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
@Input() content;
}
<app-tabs>
<app-tab></app-tab>
</app-tabs>
Вы можете подумать, что через 3 секунды Angular обновит компонент вкладки новым содержимым.
В конце концов, мы обновляем входную ссылку компонента onPush, что вызовет обнаружение изменений, верно?
Однако в данном случае это не работает. Angular не знает, что мы обновляем свойство ввода компонента вкладки, определенное в шаблоне.input()
это единственный способ сообщить Angular, что это свойство должно быть проверено в цикле обнаружения изменений.
Например:
<app-tabs>
<app-tab [content]="content"></app-tab>
</app-tabs>
Потому что, когда мы явно определяем в шаблонеinput()
, Angular создастupdateRenderer()
метод, который отслеживает значение содержимого в каждом цикле обнаружения изменений.
Простое решение в этом случае использует сеттер, а затем вызываетmarkForCheck()
.
@Component({
selector: 'app-tab',
template: `
{{_content}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
_content;
@Input() set content(value) {
this._content = value;
this.cdr.markForCheck();
}
constructor(private cdr: ChangeDetectorRef) {}
}
💪 === onPush++
в пониманииonPush
После питания давайте использовать его для создания более производительного приложения. Чем больше компонентов onPush, тем меньше проверок нужно выполнять Angular. Давайте посмотрим на реальный пример:
у нас есть другойtodos
компонент, который имеет todos как input().
@Component({
selector: 'app-todos',
template: `
<div *ngFor="let todo of todos">
{{todo.title}} - {{runChangeDetection}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
@Input() todos;
get runChangeDetection() {
console.log('TodosComponent - Checking the view');
return true;
}
}
@Component({
template: `
<button (click)="add()">Add</button>
<app-todos [todos]="todos"></app-todos>
`
})
export class AppComponent {
todos = [{ title: 'One' }, { title: 'Two' }];
add() {
this.todos = [...this.todos, { title: 'Three' }];
}
}
Недостатком описанного выше подхода является то, что когда мы нажимаем кнопку «Добавить», Angular должен проверять каждую задачу, даже если предыдущие данные не изменились. Так после первого клика в консоли появятся три лога.
В приведенном выше примере нужно проверить только одно выражение, но представьте, если бы это был реальный компонент с несколькими привязками (ngIf, ngClass, выражение и т. д.), это было бы очень дорого.
Мы провели обнаружение изменений даром!
Более эффективный способ — создать компонент todo и определить его стратегию обнаружения изменений как onPush. Например:
@Component({
selector: 'app-todos',
template: `
<app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
@Input() todos;
}
@Component({
selector: 'app-todo',
template: `{{todo.title}} {{runChangeDetection}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
@Input() todo;
get runChangeDetection() {
console.log('TodoComponent - Checking the view');
return true;
}
}
Теперь, когда мы нажимаем кнопку «Добавить», мы видим только в журнале консоли, потому что ни одна из входов других компонентов TODO не изменилась, поэтому их представления не проверяются.
И, создавая более мелкие компоненты, наш код становится более читабельным и пригодным для повторного использования.