优化 Angular 的变更检测

实施更快的变更检测,以获得更好的用户体验。

Angular 定期运行其 变更检测机制,以便数据模型的更改反映在应用程序的视图中。变更检测可以手动触发,也可以通过异步事件触发(例如,用户交互或 XHR 完成)。

变更检测是一个强大的工具,但如果运行过于频繁,则会触发大量计算并阻塞浏览器主线程。

在这篇文章中,您将学习如何通过跳过应用程序的某些部分并在必要时才运行变更检测来控制和优化变更检测机制。

Angular 变更检测的内部原理

要了解 Angular 的变更检测是如何工作的,让我们看一个示例应用程序!

您可以在 此 GitHub 存储库中找到该应用程序的代码。

该应用程序列出了公司两个部门(销售和研发部门)的员工,并具有两个组件

  • AppComponent,它是应用程序的根组件,以及
  • 两个 EmployeeListComponent 实例,一个用于销售部门,另一个用于研发部门。

Sample application

您可以在 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 的两个实例,该应用程序形成以下组件树

Component tree

AppComponent 是应用程序的根组件。它的子组件是 EmployeeListComponent 的两个实例。每个实例都有一个项目列表(E1、E2 等),这些项目代表部门中的个别员工。

当用户开始在 EmployeeListComponent 中的输入框中输入新员工的姓名时,Angular 会从 AppComponent 开始为整个组件树触发变更检测。这意味着当用户在文本输入框中键入内容时,Angular 会重复重新计算与每位员工关联的数值,以验证它们自上次检查以来是否发生了更改。

要了解这有多慢,请打开 StackBlitz 上未优化的项目版本,然后尝试输入员工姓名。

您可以通过设置 示例项目 并打开 Chrome DevTools 的 Performance 选项卡来验证减速是否来自 fibonacci 函数。

  1. 按 `Control+Shift+J` (或 Mac 上的 `Command+Option+J`) 打开 DevTools。
  2. 点击 Performance 选项卡。

现在点击 Record (在 Performance 面板的左上角),然后开始在应用程序中的一个文本框中键入内容。几秒钟后,再次点击 Record 以停止录制。一旦 Chrome DevTools 处理完它收集的所有性能分析数据,您将看到类似这样的内容

Performance profiling

如果列表中有很多员工,则此过程可能会阻塞浏览器的 UI 线程并导致帧丢失,从而导致糟糕的用户体验。

跳过组件子树

当用户在为销售 EmployeeListComponent 键入文本输入时,您知道研发部门的数据没有更改,因此没有理由在其组件上运行变更检测。为确保研发实例不触发变更检测,请将 EmployeeListComponentchangeDetectionStrategy 设置为 OnPush

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

现在,当用户在文本输入框中键入内容时,变更检测仅针对相应的部门触发

Change detection in a component subtree

您可以在 此处找到应用于原始应用程序的此优化。

您可以在 Angular 官方文档中阅读有关 OnPush 变更检测策略的更多信息。

要查看此优化的效果,请在 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 应用程序中遇到运行时减速时

  1. 使用 Chrome DevTools 分析应用程序,以查看减速来自何处。
  2. 引入 OnPush 变更检测策略以修剪组件的子树。
  3. 将繁重的计算移至纯管道,以允许框架执行计算值的缓存。