实现更快的变化检测,以提供更好的用户体验。
Angular 会定期运行其更改检测机制,以便在应用视图中反映对数据模型所做的更改。更改检测可以手动触发,也可以通过异步事件(例如用户互动或 XHR 完成)触发。
变化检测是一款功能强大的工具,但如果运行非常频繁,它可能会触发大量计算并阻止浏览器主线程。
在本文中,您将了解如何跳过应用的某些部分并仅在必要时运行更改检测,从而控制和优化更改检测机制。
Inside Angular 的更改检测
为了了解 Angular 的更改检测的工作原理,我们来看一个示例应用!
您可以在此 GitHub 代码库中找到该应用的代码。
该应用列出了公司中两个部门(销售部门和研发部门)的员工,其中包含两个部分:
AppComponent
,这是应用的根组件;以及EmployeeListComponent
的两个实例,一个用于销售,一个用于研发。
您可以在 AppComponent
的模板中看到 EmployeeListComponent
的两个实例:
<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
指令,用于在输入和 EmployeeListComponent
中声明的 label
属性之间建立双向数据绑定。
有了 EmployeeListComponent
的两个实例,应用就会形成以下组件树:
AppComponent
是应用的根组件。其子组件是两个 EmployeeListComponent
实例。每个实例都有一个内容项(E1、E2 等),用来代表部门中的个别员工。
当用户开始在 EmployeeListComponent
的输入框中输入新员工的姓名时,Angular 会针对从 AppComponent
开始的整个组件树触发更改检测。这意味着,当用户正在输入文本时,Angular 会反复重新计算与每位员工关联的数值,以验证自上次检查以来这些数值是否未发生变化。
要查看速度有多慢,请在 StackBlitz 上打开未经优化的项目版本,并尝试输入员工姓名。
您可以通过设置示例项目并打开 Chrome DevTools 的性能标签页,验证速度变慢是否是由 fibonacci
函数导致的。
- 按 `Ctrl+Shift+J`(在 Mac 上,按 `Command+Option+J`)打开开发者工具。
- 点击 Performance(效果)标签页。
现在,点击记录 (位于性能面板的左上角),开始在应用的某个文本框中输入内容。几秒钟后,再次点击记录 可停止记录。在 Chrome 开发者工具处理完其收集的所有分析数据后,您会看到如下内容:
如果列表中有很多员工,此过程可能会阻塞浏览器的界面线程并导致丢帧,从而导致糟糕的用户体验。
跳过组件子树
当用户在输入 sales EmployeeListComponent
的文本输入时,您知道 R&D 部门中的数据不会发生变化,因此没有理由对其组件运行更改检测。为了确保研发实例不会触发更改检测,请将 EmployeeListComponent
的 changeDetectionStrategy
设置为 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 仍会为该部门中的所有员工重新计算数值。
要改进此行为,您可以使用纯管道。纯管道和非净管道都接受输入,并返回可在模板中使用的结果。两者的区别在于,只有在收到与上次调用不同的输入时,纯管道才会重新计算其结果。
请注意,应用会调用 EmployeeListComponent
中定义的 calculate
方法,根据员工的数值计算要显示的值。如果您将计算移至纯管道,那么只有当管道表达式的参数发生变化时,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
更改检测策略,以修剪组件的子树。 - 将繁重的计算移至纯管道,使框架能够缓存计算的值。