實作更快速的變更偵測功能,改善使用者體驗。
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
的輸入方塊中輸入新員工的名稱時,Angular 會針對從 AppComponent
開始的整個元件樹狀結構觸發變更偵測。也就是說,當使用者在文字輸入框中輸入內容時,Angular 會重複重新計算與每位員工相關聯的數值,以確認這些數值自上次檢查後並未變更。
如要瞭解這項作業的速度,請在 StackBlitz 上開啟未經最佳化的專案版本,然後嘗試輸入員工名稱。
您可以設定範例專案,並開啟 Chrome 開發人員工具的「Performance」分頁,確認速度變慢是因為 fibonacci
函式。
- 按下 `Control + Shift + J` 鍵 (在 Mac 上為 `Command + Option + J` 鍵) 開啟開發人員工具。
- 按一下「成效」分頁標籤。
接著,請按一下「Performance」面板左上角的「Record」,然後開始在應用程式中的其中一個文字方塊中輸入內容。幾秒後,再次按一下「Record」 即可停止錄製。Chrome 開發人員工具處理收集到的所有分析資料後,您會看到類似以下的畫面:
如果清單中有許多員工,這個程序可能會阻斷瀏覽器的 UI 執行緒,導致影格掉落,進而導致使用者體驗不佳。
略過元件子樹
當使用者在「銷售」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
變更偵測策略,以便修剪元件的子樹狀結構。 - 將大量運算移至純管道,讓架構執行計算值快取作業。