实现更快的变化检测,以提供更好的用户体验。
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 开发者工具的 Performance 标签页来验证运行速度变慢是否来自 fibonacci
函数。
- 按 `Ctrl+Shift+J`(在 Mac 上,按 `Command+Option+J`)打开开发者工具。
- 点击 Performance(性能)标签页。
现在,点击 Record(记录)(位于 Performance 面板的左上角),然后在应用的某个文本框中开始输入内容。几秒钟后,再次点击录制 即可停止录制。在 Chrome 开发者工具处理完其收集的所有分析数据后,您会看到如下内容:
如果列表中有很多员工,此过程可能会阻塞浏览器的界面线程并导致丢帧,从而导致糟糕的用户体验。
跳过组件子树
当用户输入销售 EmployeeListComponent
的文本输入时,您知道研发部门的数据没有变化,因此没有理由对其组件运行更改检测。为了确保研发实例不会触发更改检测,请将 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 开发者工具分析应用,查看速度变慢的原因。
- 引入了
OnPush
更改检测策略,以剪除组件的子树。 - 将繁重的计算移至纯管道,使框架能够缓存计算的值。