Change Detection

Angular的變更檢測是框架核心功能之一,負責確保畫面上顯示的內容與底層資料保持同步。

避免 Zone.js 監控 - runOutsideAngular

這個方法可以讓特定程式碼執行不受到 Zone.js 的監控,有效避免不必要的變更檢測:

private zone = inject(NgZone);

this.zone.runOutsideAngular(() => {
  // 這裡的程式碼不會觸發變更檢測
  // 適合用於頻繁執行但不需要更新UI的操作,例如:
  // - 第三方套件初始化
  // - 高頻率事件處理(如滾動、調整大小)
  // - 動畫
});

OnPush 變更檢測策略

Angular 提供兩種變更檢測策略:

  • 預設策略 (ChangeDetectionStrategy.Default) - 較積極的檢測策略

  • OnPush 策略 (ChangeDetectionStrategy.OnPush) - 效能優化策略

如何使用 OnPush

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

@Component({
  selector: 'app-messages',
  standalone: true,
  templateUrl: './messages.component.html',
  styleUrl: './messages.component.css',
  imports: [MessagesListComponent, NewMessageComponent],
  changeDetection: ChangeDetectionStrategy.OnPush, // 關鍵設定
})
export class MessagesComponent {
  messages = signal<string[]>([]);
  
  // ...其他程式碼
}

OnPush 變更檢測的觸發條件

使用 OnPush 策略時,變更檢測僅在以下情況觸發:

  1. @Input() 參考改變 - 當輸入屬性的參考變更(而非內部屬性變更)

  2. 事件處理 - 如使用者點擊事件等DOM事件

  3. Signal 變更 - Signal 值的改變會觸發

  4. 手動觸發 - 使用 ChangeDetectorRef 手動標記

  5. Observable 和 async pipe - 使用 async pipe 訂閱 Observable 時有新值發出

使用 Signal 搭配 OnPush

Signal 是 Angular 14+ 引入的反應式狀態管理方式,與 OnPush 策略結合使用效果讚讚:

@Component({
  // ...
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessagesComponent {
  messages = signal<string[]>([]);

  onAddMessage(message: string) {
    // Signal 更新會自動觸發變更檢測,即使使用 OnPush
    this.messages.update((oldMessages) => [...oldMessages, message]);
  }
}

手動觸發變更檢測

在特定情況下,需要手動觸發變更檢測,尤其是使用外部服務或非 Signal 的資料源:

import { Component, ChangeDetectionStrategy, inject, ChangeDetectorRef } from '@angular/core';

@Component({
  // ...
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessagesListComponent implements OnInit {
  private messageService = inject(MessagesService);
  private cdRef = inject(ChangeDetectorRef);
  messages: string[] = [];

  ngOnInit(): void {
    this.messageService.message$.subscribe((message) => {
      this.messages = message;
      this.cdRef.markForCheck(); // 手動標記此組件需要檢查
    });
  }
}

ChangeDetectorRef 提供的方法:

  • markForCheck() - 標記此組件及其祖先需要檢查

  • detectChanges() - 立即檢查此組件及其子項

  • detach() - 將組件分離出變更檢測樹

  • reattach() - 重新附加組件到變更檢測樹

訂閱管理和清理

使用 destroyRef 清理訂閱

ngOnInit(): void {
  const subscription = this.messageService.message$.subscribe((message) => {
    this.messages = message;
    this.cdRef.markForCheck();
  });

  this.destroyRef.onDestroy(() => {
    subscription.unsubscribe();
  });
}

使用 takeUntilDestroyed 操作符 (Angular 16+)

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

constructor() {
  this.messageService.message$
    .pipe(takeUntilDestroyed())
    .subscribe((message) => {
      this.messages = message;
      this.cdRef.markForCheck();
    });
}

使用 Async Pipe

Async Pipe 是處理 Observable 最簡潔的方式,它會自動設置訂閱並在組件銷毀時自動清理:

// Component 程式碼
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessagesListComponent {
  messages$ = this.messageService.message$; // 直接使用 Observable
}
<!-- 模板 -->
<ul>
  @for (message of messages$ | async; track message) {
    <li>{{ message }}</li>
  }
</ul>

效能建議

  1. 為大多數組件使用 OnPush 策略

  2. 對頻繁變更但不影響 UI 的操作使用 runOutsideAngular

  3. 優先考慮使用 Signal 或 async pipe 處理資料

  4. 避免在模板中使用複雜計算,需要時使用 pure pipes 或 computed signals

  5. 組件銷毀時務必清理訂閱,避免記憶體洩漏

Last updated