iT邦幫忙

0

訊號 的 Local Change Detection

  • 分享至 

  • xImage
  •  

Change Detection 的歷史

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:

  • 它接收新的輸入,或
  • 由 AsyncPipe 解析的 Observable,或者
  • 它運行一個 event listener,或者
  • 訊號更新。

OnPush change strategy 優化了效能,但 Angular 17 引入了 local change detection (局部變化偵測),它只會更新訊號更新的元件,而忽略元件樹的其餘部分。

Default Change Strategy(始終運行)

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();
 }
}

AppComponentDefaultChildComponent 元件的父元件,它有 OnPush change strategy。

場景 1:按鈕點選發生在 DefaultGrandChildComponent 元件中

該元件被標記為 dirty。當偵測到變化時,此元件會增加 count 訊號並顯示當前時間。 DefaultChildComponent 元件是其父元件;因此,DefaultChildComponent 被標記為 dirty。 AppComponentDefaultChildComponent 元件的父元件;因此,它被標記為 dirty。 OnPushChildComponent 的 change strategy 是 OnPush;它沒有接收新的輸入,event listener 沒有運行,沒有 AsyncPipe,也沒有更新訊號。因此,它不會被標記為 dirty,並且其子樹 (subtree) 會被忽略。

當 change detection 發生時,AppComponentDefaultChildComponentDefaultGrandChildComponent 元件會更新。

場景二:按鈕點選發生在DefaultChildComponent元件中

類似地,DefaultChildComponentAppComponent 元件也被標記為 dirty。 DefaultGrandChildComponent 具有 Default change strategy;因此,儘管其狀態沒有改變,但它被標記為 dirty。

當 change detection 發生時,AppComponentDefaultChildComponentDefaultGrandChildComponent 元件會更新。 Default change strategy 效能不佳,因為 DefaultChildComponent 的子樹 (subtree) 始終運行。 當其子樹增長時,應用程式會變得緩慢,因為所有元件都被標記為 dirty,並且更新所有元件。

OnPush 變更策略(效能最佳化)

@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 訊號。 readValuevalue 訊號的唯讀訊號。

@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 方法來顯示當前時間。

場景 1:按鈕點選發生在 OnPushGrandChildComponent 元件中

該元件被標記為 dirty。當偵測到變化時,此元件會增加 counter 訊號並顯示當前時間。 OnPushChildComponent 是它的父元件;因此,OnPushChildComponent 被標記為 dirty。 AppComponentOnPushChildComponent 元件的父元件;因此,它被標記為 dirty。 DefaultChildComponent 的 change strategy 是 Default;它的子樹 (subtree) 總是被更新。

當 change detection 發生時,AppComponentDefaultChildComponentDefaultGrandChildComponentOnPushChildComponentOnPushGrandChildComponent 元件都會更新。

場景二:按鈕點擊發生在 OnPushChildComponent 元件中

類似地,AppComponentDefaultChildComponent 的子樹也被標記為 dirty。 OnPushGrandChildComponent 具有 OnPush change strategy,並且不符合任何 change detectio 標準。它沒有接收新的輸入,沒有運作 event listener,沒有 AsyncPipe,也沒有訊號更新;因此,它沒有被標記為 dirty。

當變更偵測發生時,AppComponentDefaultChildComponentDefaultGrandChildComponentOnPushChildComponent 元件會更新。 OnPush change strategy 優化了應用程式的效能,因為當子樹 (subtree) 不符 change detection 標準時,它們不會執行 change detection。當子樹增長時,只有根和觸發事件的元件之間的元件才會被標記為 dirty 並更新。受影響組件的數量顯著減少。

本地變化檢測 (Local Change Detection)

在 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 元件點擊按鈕時,它會增加 CounterServicevalue 訊號。 由於運行了 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 元件在模板中顯示 CounterServicevalue 訊號;因此將其標記為dirty。 它的父元件 OnPushChildComponent 並未因為 local change detection 而被標記為 dirty 元件。

假設 DefaultChildComponentDefaultGrandChildComponent 不存在,則只更新 AppComponentOnPushGrandChildComponent 元件。如果在 OnPushChildComponentOnPushGrandChildComponent 之間插入更多元件,local change detection 將確保這些元件不會被標記為 dirty。 Change detection 的數量固定為三個;一個用於 AppComponent,另外兩個用於兩個 OnPushGrandChildComponent 元件。

我們應該從 local change detection 中獲益,並在現代 Angular 開發中使用訊號來實現 reactivity,

參考:


尚未有邦友留言

立即登入留言