优化 Angular 的更改检测

实现更快的变化检测,以提供更好的用户体验。

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 函数。

  1. 按 `Ctrl+Shift+J`(在 Mac 上,按 `Command+Option+J`)打开开发者工具。
  2. 点击 Performance(性能)标签页。

现在,点击 Record(记录)(位于 Performance 面板的左上角),然后在应用的某个文本框中开始输入内容。几秒钟后,再次点击录制 即可停止录制。在 Chrome 开发者工具处理完其收集的所有分析数据后,您会看到如下内容:

性能剖析

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

跳过组件子树

当用户输入销售 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 {...}

现在,当用户输入文本时,系统仅会针对相应部门触发更改检测:

组件子树中的更改检测

您可以点击此处查看已应用于原始申请的该优化。

如需详细了解 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 应用中遇到运行时运行缓慢的问题时:

  1. 使用 Chrome 开发者工具分析应用,查看速度变慢的原因。
  2. 引入了 OnPush 更改检测策略,以剪除组件的子树。
  3. 将繁重的计算移至纯管道,使框架能够缓存计算的值。