隨著子元件的增多和巢狀,遇到資料改變,但是元件頁面沒有渲染的問題開始變多。
例如:
此頁面為子元件,開啟該頁面後,原本應該顯示已存在的附件,但是現在卻顯示空。
正確顯示:
問題顯示:沒有渲染
此問題的出現往往是因為變更檢測器沒有檢測到元件的資料變更,沒有進行變更檢測。
到底為什麼沒有檢測到,探究了一段時間也沒有探究出發生問題根本原因是什麼。
但往往可以過手動變更檢測來解決,如下述程式碼的第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
}
假設我們有如下元件樹
detach()
允許我們操作狀態的第一個方法是detach
,它只是單純禁用對當前檢視的檢測。
使用方法也很簡單。
export class AComponent {
constructor(public cd: ChangeDetectorRef) {
this.cd.detach();
}
這確保了在執行以下更改檢測時,AppComponent將跳過以 開頭的左分支(不會檢查背景為黃色的元件)。
同時假如AComponent的狀態發生了改變,它的子元件也不會進行檢查。
reattach
將先前分離的檢視重新附加到更改檢測樹。
例如我們使用reattach()
方法,就可以將上面使用detach()禁用的檢視重新新增進來。
例如:
@Input()
set live(value: boolean) {
if (value) {
this.ref.reattach();
} else {
this.ref.detach();
}
}
markForCheck
該方法適用於使用OnPush
策略的時候。
當檢視使用OnPush (checkOnce) 更改檢測策略時,顯式將檢視標記為髒,以便再次對其進行檢查。
從搜尋到的資料來看,它只是向上迭代並啟用對每個父元件直至根的檢查。
detectChanges
對當前元件及其所有子元件執行一次更改檢測, 也是我們最常用的。
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類,可以很好地幫助我們對變更檢測的原理和行為,當預設變更檢測滿足不了我們的想法時,可以讓我們手動地去調整檢視的更新。