angular ChangeDetectorRef

weiewiyi發表於2022-06-27

隨著子元件的增多和巢狀,遇到資料改變,但是元件頁面沒有渲染的問題開始變多。

例如:
此頁面為子元件,開啟該頁面後,原本應該顯示已存在的附件,但是現在卻顯示空。
正確顯示:

image.png

問題顯示:沒有渲染

image.png

此問題的出現往往是因為變更檢測器沒有檢測到元件的資料變更,沒有進行變更檢測。

到底為什麼沒有檢測到,探究了一段時間也沒有探究出發生問題根本原因是什麼。

但往往可以過手動變更檢測來解決,如下述程式碼的第9行,呼叫ChangeDetectorRef的detectChanges函式。今天就來講一講關於ChangeDetectorRef的相關知識。

1 constructor(private attachmentService: AttachmentService,
2              private ref: ChangeDetectorRef) {
3  }

4 getAttachmentByIds() {
5 this.attachmentService.getAttachmentByIds(this.attachmentIds)
6      .subscribe(attachments => {
7        this.attachments = attachments;
8        // 進行強制變更檢測 防止頁面不顯示attachments的變更
9        this.ref.detectChanges();
10      })
11  }

變更檢測

先來說說angular變更檢測的兩種策略。

  • Default
  • OnPush

Default策略

Angular的元件可以依賴其他的元件來構建應用程式的頁面邏輯,最後形成一棵元件樹。每個元件都有自己的變更檢測器(change detector)。因此,變更檢測器的結構也是一棵同構的樹


當某個元件的狀態發生改變時,Angular會從這棵樹的根節點開始遍歷,出發所有元件節點的變更檢測器,這樣Angular就知道那些元件的狀態發生了改變,需要更新相應的UI。

這個過程看似開銷很大,但Angular已經進行了大量優化,實際變更檢測的速度很快。 這種策略在我們應用元件過多時會對我們的應用產生效能的影響, 不過在不熟悉相關細節的情況下,Default策略是我們最好的選擇。

OnPush策略

OnPush策略我目前沒有嘗試用過,但可以作為了解。

Angular 還提供了一種 OnPush 策略,我們可以修改元件裝飾器的 changeDetection 屬性來更改變化檢測的策略。 如下述程式碼的第4行。

1 @Component({
2    selector: 'app-A',
3    // 設定變化檢測的策略
4    changeDetection: ChangeDetectionStrategy.OnPush,
5    template: ...
6 })
7 export class AComponent {
8    ...
9 }

OnPush策略下,只有這幾種情況可以觸發當前元件的變更檢測:

  • 元件的輸入屬性(繫結)的引用被改變
  • 元件內部觸發了非同步事件
  • 手動觸發變更檢測
  • 當前元件或子元件之一觸發了事件, 如click

簡單談談第一點,這是angular中比較經典的處理方法。其他的都比較直觀。

例如父元件向子元件使用@Input傳入一個物件

@Component({
template: `
<child [people]="people"></child>
`
})
export class AppComponent  {
people = {
name: '張三'
};
onClick1() {
this.people.name = '李四';
}
onClick2() {
this.people = { name: '李四'};
}
}

父元件呼叫onClick1函式並不會觸發變更檢測,因為這僅僅是改變了物件的屬性,並沒有改變物件的引用。

而onClick2函式才會觸發變更檢測。


我們可以通過以下的圖觀察onPush策略下的行為。

當預設變更檢測進行時,變更檢測器並沒有去更新onPush策略那一邊的子樹。在我們對元件的變更檢測十分了解的情況下,使用這種行為可以減少不必要的變更檢測從而提高效能。

總結:
為了自動檢測變化,Angular 預設使用 ChangeDetectionStrategy.Default 策略,可確保我們的 UI 以可預測和高效能的方式顯示,在變更元件不超過50個時,適用於大多數應用程式
對於較大的應用程式,可以考慮使用 ChangeDetectionStrategy.OnPush策略。

ChangeDetectorRef

接下來說說手動變更檢測。

手動變更檢測使用到了angular給我們提供的ChangeDetectorRef類,定義了以下幾種公共介面。

class ChangeDetectorRef {  
  markForCheck() : void  
  detach() : void  
  reattach() : void  

  detectChanges() : void  
  checkNoChanges() : void  
}

假設我們有如下元件樹

image.png


detach()

允許我們操作狀態的第一個方法是detach,它只是單純禁用對當前檢視的檢測。

使用方法也很簡單。

export class AComponent {  
  constructor(public cd: ChangeDetectorRef) {  
    this.cd.detach();
  }

這確保了在執行以下更改檢測時,AppComponent將跳過以 開頭的左分支(不會檢查背景為黃色的元件)。

同時假如AComponent的狀態發生了改變,它的子元件也不會進行檢查。
image.png

reattach

將先前分離的檢視重新附加到更改檢測樹。

例如我們使用reattach()方法,就可以將上面使用detach()禁用的檢視重新新增進來。

例如:

@Input()
  set live(value: boolean) {
    if (value) {
      this.ref.reattach();
    } else {
      this.ref.detach();
    }
  }

image.png

markForCheck

該方法適用於使用OnPush策略的時候。

當檢視使用OnPush (checkOnce) 更改檢測策略時,顯式將檢視標記為髒,以便再次對其進行檢查。

從搜尋到的資料來看,它只是向上迭代並啟用對每個父元件直至根的檢查。

image.png

detectChanges

對當前元件及其所有子元件執行一次更改檢測, 也是我們最常用的。

image.png

checkNoChanges

可確保在當前的變更檢測執行中不會發生任何更改。如果發現更改的繫結或確定應該更新 DOM,則丟擲異常。

組合操作

比如元件的資料預計會不斷變化,每秒多次。為了提高效能,我們希望檢查和更新列表的頻率低於實際發生更改的頻率。為此,我們可以分離元件的更改檢測器並每五秒執行一次檢查。

@Component({
  selector: 'giant-list',
  template: `
      <li *ngFor="let d of dataProvider.data">Data {{d}}</li>
    `,
})
class GiantList {
  constructor(private ref: ChangeDetectorRef, public dataProvider: DataListProvider) {
    ref.detach();
    setInterval(() => {
      this.ref.detectChanges();
    }, 5000);
  }
}

總結: 理解ChangeDetectorRef類,可以很好地幫助我們對變更檢測的原理和行為,當預設變更檢測滿足不了我們的想法時,可以讓我們手動地去調整檢視的更新。

相關文章