Angular 團隊在企業應用程式的效能方面做出了許多改進。 組件中的 Change Detection 的預設值是ChangeDetection.Default
。這意味著組件無論其狀態如何,總是運行 Change Detection。 當使用預設值時,未修改的元件必須不必要地執行 Change Detection。當應用程式具有大型元件樹 (component tree) 時,這可能會損害應用程式的效能,其中一個變更可能會觸發所有元件執行 Change Detection。
然後,團隊引入了 OnPush
change strategy,減少了組件樹中組件之間的 change detection 次數。
@Component({
selector: 'app-on-push-grand-child',
template: `...inline template…`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandChildComponent {}
當元件加入 changeDetection: ChangeDetectionStrategy.OnPush
時,它使用 OnPush
change strategy 來執行 change detection。在以下情況下,元件將運行 change detection:
OnPush
change strategy 優化了效能,但 Angular 17 引入了 local change detection (局部變化偵測),它只會更新訊號更新的元件,而忽略元件樹的其餘部分。
Change strategy 的預設值為 Default
。當元件具有 Default change strategy 時,無論是否已修改,change detection 都會運作。我們舉個例子來解釋一下。
export function getCurrentTime(): string {
return new Date(Date.now()).toISOString();
}
@Component({
selector: 'app-default-grand-child',
template: `
<p>{{ showCurrentTime() }}</p>
<p>Counter: {{ count() }}</p>
<button (click)="add()">Add</button>`
})
export class DefaultGrandChildComponent {
count = signal(0);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
DefaultGrandChildComponent
元件具有 Default
change strategy。它有一個增加 count
訊號的按鈕,showCurrentTime
方法顯示當前時間。
@Component({
selector: 'app-default-child',
imports: [DefaultGrandChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<p>Internal Count: {{ count() }}</p>
<button (click)="add()">Add</button>
<app-default-grand-child />`
})
export class DefaultChildComponent {
count = signal(0);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
DefaultChildComponent
元件也具有 Default
change strategy,並且是 DefaultGrandChildComponent
元件的父元件。類似地,它有一個按鈕來增加 count
訊號和 showCurrentTime
來顯示當前時間。
@Component({
selector: 'app-root',
imports: [OnPushChildComponent, DefaultChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<button (click)="counterService.add()">Add CounterService value</button>
<app-on-push-child />
<app-on-push-child />
<app-default-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
counterService = inject(CounterService);
showCurrentTime() {
return getCurrentTime();
}
}
AppComponent
是 DefaultChildComponent
元件的父元件,它有 OnPush
change strategy。
該元件被標記為 dirty。當偵測到變化時,此元件會增加 count
訊號並顯示當前時間。 DefaultChildComponent
元件是其父元件;因此,DefaultChildComponent
被標記為 dirty。 AppComponent
是 DefaultChildComponent
元件的父元件;因此,它被標記為 dirty。 OnPushChildComponent
的 change strategy 是 OnPush
;它沒有接收新的輸入,event listener 沒有運行,沒有 AsyncPipe,也沒有更新訊號。因此,它不會被標記為 dirty,並且其子樹 (subtree) 會被忽略。
當 change detection 發生時,AppComponent
、 DefaultChildComponent
和 DefaultGrandChildComponent
元件會更新。
類似地,DefaultChildComponent
和 AppComponent
元件也被標記為 dirty。 DefaultGrandChildComponent
具有 Default
change strategy;因此,儘管其狀態沒有改變,但它被標記為 dirty。
當 change detection 發生時,AppComponent
、DefaultChildComponent
和 DefaultGrandChildComponent
元件會更新。 Default change strategy 效能不佳,因為 DefaultChildComponent
的子樹 (subtree) 始終運行。 當其子樹增長時,應用程式會變得緩慢,因為所有元件都被標記為 dirty,並且更新所有元件。
@Injectable({
providedIn: 'root'
})
export class CounterService {
private readonly value = signal(0);
readValue = this.value.asReadonly();
add(delta=1) {
this.value.update((prev) => prev + delta);
}
}
CounterService
服務具有 add
方法增加的 value
訊號。 readValue
是 value
訊號的唯讀訊號。
@Component({
selector: 'app-on-push-grand-child',
template: `
<p>{{ showCurrentTime() }}</p>
<p>Internal Counter: {{ count() }}</p>
<p>Counter Service value: {{ counterService.readValue() }}</p>
<button (click)="add()">Add</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandChildComponent {
count = signal(0);
counterService = inject(CounterService);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
OnPushGrandChildComponent
元件具有 OnPush
change strategy。它有一個增加 count
訊號的按鈕,showCurrentTime
方法顯示當前時間。此外,它還注入了 CounterService
來顯示 readValue
訊號的值。
@Component({
selector: 'app-on-push-child',
imports: [OnPushGrandChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<p>Internal Count: {{ count() }}</p>
<button (click)="add()">Add</button>
<app-on-push-grand-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushChildComponent {
count = signal(0);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
OnPushChildComponent
元件也具有 OnPush
change strategy,並且是 OnPushGrandChildComponent
元件的父元件。類似地,它有一個按鈕來增加 count
訊號和 showCurrentTime
方法來顯示當前時間。
該元件被標記為 dirty。當偵測到變化時,此元件會增加 counter
訊號並顯示當前時間。 OnPushChildComponent
是它的父元件;因此,OnPushChildComponent
被標記為 dirty。 AppComponent
是 OnPushChildComponent
元件的父元件;因此,它被標記為 dirty。 DefaultChildComponent
的 change strategy 是 Default
;它的子樹 (subtree) 總是被更新。
當 change detection 發生時,AppComponent
、DefaultChildComponent
、DefaultGrandChildComponent
、OnPushChildComponent
和 OnPushGrandChildComponent
元件都會更新。
類似地,AppComponent
和 DefaultChildComponent
的子樹也被標記為 dirty。 OnPushGrandChildComponent
具有 OnPush
change strategy,並且不符合任何 change detectio 標準。它沒有接收新的輸入,沒有運作 event listener,沒有 AsyncPipe,也沒有訊號更新;因此,它沒有被標記為 dirty。
當變更偵測發生時,AppComponent
、DefaultChildComponent
、DefaultGrandChildComponent
和 OnPushChildComponent
元件會更新。 OnPush
change strategy 優化了應用程式的效能,因為當子樹 (subtree) 不符 change detection 標準時,它們不會執行 change detection。當子樹增長時,只有根和觸發事件的元件之間的元件才會被標記為 dirty 並更新。受影響組件的數量顯著減少。
在 Angular 17 中,團隊為訊號添加了 local change detection
。訊號更新時,只有一個組件被標記為 dirty 並更新。
@Component({
selector: 'app-root',
imports: [OnPushChildComponent, DefaultChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<button (click)="counterService.add()">Add CounterService value</button>
<div class="child" >
<app-on-push-child />
<app-on-push-child />
<app-default-child />
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
counterService = inject(CounterService);
showCurrentTime() {
return getCurrentTime();
}
}
當 AppComponent
元件點擊按鈕時,它會增加 CounterService
的 value
訊號。 由於運行了 event listener,因此它被標記為 dirty。
@Component({
selector: 'app-on-push-grand-child',
template: `
<p>{{ showCurrentTime() }}</p>
<p>Counter Service value: {{ counterService.readValue() }}</p>
<button (click)="add()">Add</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandChildComponent {
count = signal(0);
counterService = inject(CounterService);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
OnPushGrandChildComponent
元件在模板中顯示 CounterService
的 value
訊號;因此將其標記為dirty。 它的父元件 OnPushChildComponent
並未因為 local change detection 而被標記為 dirty 元件。
假設 DefaultChildComponent
和 DefaultGrandChildComponent
不存在,則只更新 AppComponent
和 OnPushGrandChildComponent
元件。如果在 OnPushChildComponent
和 OnPushGrandChildComponent
之間插入更多元件,local change detection
將確保這些元件不會被標記為 dirty。 Change detection
的數量固定為三個;一個用於 AppComponent
,另外兩個用於兩個 OnPushGrandChildComponent
元件。
我們應該從 local change detection
中獲益,並在現代 Angular 開發中使用訊號來實現 reactivity,