Реализуйте более быстрое обнаружение изменений для улучшения пользовательского опыта.
Angular периодически запускает механизм обнаружения изменений , чтобы изменения в модели данных отражались в представлении приложения. Обнаружение изменений может быть запущено вручную или через асинхронное событие (например, взаимодействие с пользователем или завершение XHR-запроса).
Обнаружение изменений — мощный инструмент, но если его запускать слишком часто, он может запустить большой объем вычислений и заблокировать основной поток браузера.
В этой статье вы узнаете, как контролировать и оптимизировать механизм обнаружения изменений, пропуская части вашего приложения и запуская обнаружение изменений только при необходимости.
Обнаружение изменений в Angular изнутри
Чтобы понять, как работает обнаружение изменений в Angular, давайте рассмотрим пример приложения!
Код приложения вы можете найти в этом репозитории GitHub .
Приложение отображает список сотрудников двух отделов компании — отдела продаж и отдела исследований и разработок — и состоит из двух компонентов:
-
AppComponent
, который является корневым компонентом приложения, и - Два экземпляра
EmployeeListComponent
: один для продаж и один для НИОКР.
Вы можете увидеть два экземпляра EmployeeListComponent
в шаблоне для AppComponent
:
<app-employee-list
[data]="salesList"
department="Sales"
(add)="add(salesList, $event)"
(remove)="remove(salesList, $event)"
></app-employee-list>
<app-employee-list
[data]="rndList"
department="R&D"
(add)="add(rndList, $event)"
(remove)="remove(rndList, $event)"
></app-employee-list>
Для каждого сотрудника указано имя и числовое значение. Приложение передаёт числовое значение сотрудника в бизнес-расчёт и визуализирует результат на экране.
Теперь взгляните на EmployeeListComponent
:
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Component(...)
export class EmployeeListComponent {
@Input() data: EmployeeData[];
@Input() department: string;
@Output() remove = new EventEmitter<EmployeeData>();
@Output() add = new EventEmitter<string>();
label: string;
handleKey(event: any) {
if (event.keyCode === 13) {
this.add.emit(this.label);
this.label = '';
}
}
calculate(num: number) {
return fibonacci(num);
}
}
EmployeeListComponent
принимает список сотрудников и название отдела в качестве входных данных. При попытке пользователя удалить или добавить сотрудника компонент генерирует соответствующий выходной сигнал. Компонент также определяет метод calculate
, реализующий бизнес-расчет.
Вот шаблон для EmployeeListComponent
:
<h1 title="Department">{{ department }}</h1>
<mat-form-field>
<input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
<mat-list-item *ngFor="let item of data">
<h3 matLine title="Name">
{{ item.label }}
</h3>
<md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
{{ calculate(item.num) }}
</md-chip>
</mat-list-item>
</mat-list>
Этот код перебирает всех сотрудников в списке и для каждого из них отображает элемент списка. Он также включает директиву ngModel
для двусторонней привязки данных между входными данными и свойством label
, объявленным в EmployeeListComponent
.
С двумя экземплярами EmployeeListComponent
приложение формирует следующее дерево компонентов:
AppComponent
— корневой компонент приложения. Его дочерними компонентами являются два экземпляра EmployeeListComponent
. Каждый экземпляр содержит список элементов ( E1 , E2 и т. д.), представляющих отдельных сотрудников отдела.
Когда пользователь начинает вводить имя нового сотрудника в поле ввода EmployeeListComponent
, Angular запускает обнаружение изменений для всего дерева компонентов, начиная с AppComponent
. Это означает, что пока пользователь вводит текст, Angular многократно пересчитывает числовые значения, связанные с каждым сотрудником, чтобы убедиться, что они не изменились с момента последней проверки.
Чтобы увидеть, насколько медленно это может работать, откройте неоптимизированную версию проекта на StackBlitz и попробуйте ввести имя сотрудника.
Вы можете убедиться, что замедление вызвано функцией fibonacci
, настроив пример проекта и открыв вкладку «Производительность» в Chrome DevTools.
- Нажмите `Control+Shift+J` (или `Command+Option+J` на Mac), чтобы открыть DevTools.
- Откройте вкладку «Производительность» .
Теперь нажмите «Запись». (в левом верхнем углу панели «Производительность» ) и начните вводить текст в одном из текстовых полей приложения. Через несколько секунд нажмите кнопку «Запись». Нажмите ещё раз, чтобы остановить запись. После того, как Chrome DevTools обработает все собранные данные профилирования, вы увидите что-то вроде этого:
Если в списке много сотрудников, этот процесс может заблокировать поток пользовательского интерфейса браузера и вызвать пропуски кадров, что приведет к ухудшению пользовательского опыта.
Пропуск поддеревьев компонентов
Когда пользователь вводит текст в поле «Продажи» для компонента EmployeeListComponent
вы знаете, что данные в отделе исследований и разработок не меняются, поэтому нет смысла запускать обнаружение изменений для этого компонента. Чтобы экземпляр отдела исследований и разработок не запускал обнаружение изменений, установите для параметра changeDetectionStrategy
компонента EmployeeListComponent
значение OnPush
:
import { ChangeDetectionStrategy, ... } from '@angular/core';
@Component({
selector: 'app-employee-list',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}
Теперь, когда пользователь вводит текстовое поле, обнаружение изменений срабатывает только для соответствующего отдела:
Эту оптимизацию, примененную к исходному приложению, можно найти здесь .
Подробнее о стратегии обнаружения изменений OnPush
можно прочитать в официальной документации Angular .
Чтобы увидеть эффект от этой оптимизации, введите нового сотрудника в заявку на StackBlitz .
Использование чистых труб
Несмотря на то, что стратегия обнаружения изменений для EmployeeListComponent
теперь установлена на OnPush
, Angular по-прежнему пересчитывает числовое значение для всех сотрудников в отделе, когда пользователь вводит соответствующее текстовое поле.
Для улучшения этого поведения можно воспользоваться преимуществами чистых каналов . Как чистые, так и нечистые каналы принимают входные данные и возвращают результаты, которые можно использовать в шаблоне. Разница между ними заключается в том, что чистый канал пересчитывает результат только в том случае, если получает входные данные, отличные от данных предыдущего вызова.
Помните, что приложение вычисляет отображаемое значение на основе числового значения сотрудника, вызывая метод calculate
определённый в EmployeeListComponent
. Если перенести вычисление в чистый конвейер, Angular будет пересчитывать выражение конвейера только при изменении его аргументов. Фреймворк определит, изменились ли аргументы конвейера, выполняя проверку ссылок. Это означает, что Angular не будет выполнять перерасчёты, пока числовое значение для сотрудника не обновится.
Вот как перенести бизнес-вычисления в канал под названием CalculatePipe
:
import { Pipe, PipeTransform } from '@angular/core';
const fibonacci = (num: number): number => {
if (num === 1 || num === 2) {
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
};
@Pipe({
name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
transform(val: number) {
return fibonacci(val);
}
}
Метод transform
канала вызывает функцию fibonacci
. Обратите внимание, что канал является чистым. Angular будет считать все каналы чистыми, если не указано иное.
Наконец, обновите выражение внутри шаблона для EmployeeListComponent
:
<mat-chip-list>
<md-chip>
{{ item.num | calculate }}
</md-chip>
</mat-chip-list>
Вот и всё! Теперь, когда пользователь вводит текстовое поле, связанное с любым отделом, приложение не будет пересчитывать числовое значение для отдельных сотрудников.
В приложении ниже вы можете увидеть, насколько плавнее стал набор текста!
Чтобы увидеть эффект последней оптимизации , попробуйте этот пример на StackBlitz .
Код с чистой конвейерной оптимизацией исходного приложения доступен здесь .
Заключение
При столкновении с замедлением выполнения приложения Angular:
- Профилируйте приложение с помощью Chrome DevTools, чтобы увидеть причины замедления.
- Внедрить стратегию обнаружения изменений
OnPush
для очистки поддеревьев компонента. - Перенесите сложные вычисления в чистые каналы, чтобы позволить фреймворку выполнять кэширование вычисленных значений.