最佳化 Angular's 變化偵測

執行更快速的變更偵測技術,以改善使用者體驗。

Angular 會定期執行其變更偵測機制,讓應用程式的檢視畫面反映資料模型的變更。變更偵測可以手動或非同步事件 (例如使用者互動或 XHR 完成) 來觸發。

變更偵測是一項強大工具,但若頻繁執行,可能會觸發許多運算作業,並封鎖主瀏覽器執行緒。

在這篇文章中,您將瞭解如何控制及最佳化變更偵測機制,包括略過應用程式的某些部分,以及只在必要時執行變更偵測。

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 的輸入框中輸入新員工姓名時,從 AppComponent 開始,Angular 會觸發整個元件樹狀結構的變更偵測作業。也就是說,當使用者輸入文字時,Angular 會重複計算與每位員工相關的數值,確認自上次檢查後就未曾變更。

如要瞭解這項作業的速度有多慢,請開啟 StackBlitz 的未最佳化專案版本,然後輸入員工名稱。

您可以設定範例專案,並開啟 Chrome 開發人員工具的「效能」分頁,藉此確認 fibonacci 函式是否速度變慢。

  1. 按下 `Control+Shift+J 鍵 (在 Mac 上為 Command+Option+J 鍵) 開啟開發人員工具。
  2. 按一下「成效」分頁標籤。

即可停止錄製。Chrome 開發人員工具處理完收集到的所有剖析資料後,您會看到類似下方的內容:

效能分析

如果清單中有多名員工,這項程序可能會封鎖瀏覽器的 UI 執行緒,進而導致影格遺失,進而對使用者體驗造成負面影響。

略過元件子樹狀結構

當使用者在銷售 EmployeeListComponent 輸入文字時,就知道「研發」部門的資料不會改變,所以沒有理由對元件執行變更偵測功能。為確保 R&D 執行個體不會觸發變更偵測,請將 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. 將繁重的運算作業移至純管道,讓架構能夠執行所計算值的快取。