Angular髒值檢查
本文提供了您需要了解的有關變更檢測的所有必要資訊。通過使用本文構建的演示專案來解釋angular 的變更檢測機制。
Angular的變更檢測是該框架的核心機制,但(至少以我的經驗)很難理解。更不幸的是,官方網站上沒有關於此主題的官方指南。
What Is Change Detection
Angular的兩個主要目標是可預測和高效。框架需要通過組合狀態和模板來在UI上覆制應用程式的狀態:
如果狀態發生任何更改,也必須更新DOM。將HTML與我們的資料同步的機制稱為“更改檢測”。每個前端框架都使用其實現,例如React使用虛擬DOM,Angular使用更改檢測等等。我可以推薦文章“ JavaScript框架中的更改及其檢測”,該文章很好地概述了此主題。
更改檢測:資料更改後更新DOM的過程
作為開發人員,大多數時候我們不需要關心變更檢測,除非我們需要優化應用程式的效能。如果處理不當,更改檢測會降低大型應用程式的效能。
How Change Detection Works 變更檢測是如何工作的
變更檢測週期可以分為兩個部分:
- 開發人員更新應用程式模型
- Angular通過重新渲染來同步DOM中的更新模型
讓我們更詳細地看一下這個過程:
- 開發人員更新資料模型,例如通過更新元件繫結
- angular 檢測變化
- 變更檢測從上到下檢查元件樹中的每個元件,以檢視相應的模型是否已更改
- 如果有新值,它將更新元件的檢視(DOM)
以下GIF以簡化的方式演示了此過程:
該圖顯示了Angular元件樹及其在應用程式引導過程中為每個元件建立的更改檢測器(CD)。該檢測器將當前值與屬性的先前值進行比較。如果該值已更改,它將==isChanged==設定為==true==。檢查框架程式碼中的實現,這只是與NaN的特殊處理進行的===比較。
Zone.js
一般情況下,zone可以跟蹤並攔截任何非同步任務。
Zone 通常具有以下階段:
- 開始穩定
- 如果任務在區域中執行,它將變得不穩定,
- 如果任務完成,它將再次變得穩定
Angular在啟動時修補了幾個低階瀏覽器API,以便能夠檢測到應用程式中的更改。這是使用zone.js完成的,該區域修補了EventEmitter,DOM事件偵聽器,XMLHttpRequest,Node.js中的fs API等API。
簡而言之,如果發生以下事件之一,則框架將觸發更改檢測
- 任何瀏覽器事件(單擊,鍵入等)
setInterval()
andsetTimeout()
- HTTP 請求
Angular使用其稱為NgZone
的區域。僅存在一個NgZone
,並且僅針對此區域中觸發的非同步操作觸發更改檢測。
Performance 效能
預設情況下,如果模板值已更改,則“Angular Change Detection ”將從上至下檢查所有元件。
Angular對每個元件執行更改檢測的速度非常快,因為它可以使用內聯快取在毫秒內執行數千次檢查,內聯快取可生成VM優化程式碼。
如果您想對此主題有更深入的說明,建議您觀看Victor Savkin關於“重塑變化檢測”的演講。
儘管Angular在後臺進行了大量優化,但是在大型應用程式上效能仍然會下降。在下一章中,您將學習如何通過使用不同的變更檢測策略來主動提高Angular效能。
Change Detection Strategies 變更檢測策略
Angular提供了兩種策略來執行更改檢測:
Default
OnPush
讓我們看一下每種變化檢測策略。
Default Change Detection Strategy
預設情況下,Angular使用ChangeDetectionStrategy.Default更改檢測策略。每當事件觸發更改檢測(例如使用者事件,計時器,XHR,promise等)時,此預設策略都會從上到下檢查元件樹中的每個元件。這種不對元件的依賴項做任何假設的保守檢查方法稱為髒檢查。它可能會對包含許多元件的大型應用程式的效能產生負面影響。
OnPush Change Detection Strategy
通過將changeDetection屬性新增到元件裝飾器後設資料中,我們可以切換到ChangeDetectionStrategy.OnPush更改檢測策略:
@Component({
selector: 'hero-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ...
})
export class HeroCard {
...
}
這種更改檢測策略可以跳過對此元件及其所有子元件的不必要檢查。
下一個GIF演示了使用OnPush更改檢測策略跳過元件樹的各個部分:
使用此策略,Angular知道僅在以下情況下才需要更新元件:
- 輸入屬性已更改, 標記為@Input() 的屬性;
- 該元件或其子元件之一觸發事件處理程式
- 手動觸發變化檢測
通過非同步管道連結到模板的可觀察物件發出新值, 如 data | async
讓我們仔細看看這些事件型別。
Input Reference Changes
在預設的更改檢測策略中,每當@Input()資料被更改或修改時,Angular將執行更改檢測器。使用OnPush策略,僅當新引用作為@Input()值傳遞時,才會觸發更改檢測器。
JavaScript中的所有內容都是按引用傳遞的,但是所有基元都是不可變的,並且它們的文字表示均指向相同的基元例項/引用。修改物件屬性或陣列條目不會建立新引用,因此不會觸發OnPush元件上的更改檢測。要觸發變更檢測器,您需要傳遞一個新的物件或陣列引用。
您可以使用簡單DEMO測試此行為:
- 使用ChangeDetectionStrategy.Default修改HeroCardComponent的 age
- 驗證帶有ChangeDetectionStrategy.OnPush的HeroCardOnPushComponent不能反映更改的age(通過元件周圍的紅色邊框顯示)
- 在“修改英雄”皮膚中單擊“建立新物件引用”
- 驗證是否通過更改檢測檢查了具有ChangeDetectionStrategy.OnPush的HeroCardOnPushComponent
為防止更改檢測錯誤,在所有地方僅使用不可變的物件和列表使用OnPush更改檢測來構建應用程式可能會很有用。不可變物件只能通過建立新的物件引用來修改,因此我們可以保證:
- 每次更改都會觸發OnPush更改檢測
- 我們不要忘了建立一個新的物件引用,否則可能導致錯誤;
Immutable.js是一個不錯的選擇,該庫為物件(地圖)和列表(列表)提供了持久不變的資料結構。通過npm安裝庫提供了型別定義,以便我們可以在IDE中利用型別泛型,錯誤檢測和自動完成功能。
Event Handler Is Triggered
如果OnPush元件或其子元件之一觸發事件處理程式(例如單擊按鈕),則將觸發更改檢測(針對元件樹中的所有元件)。
請注意,以下操作不會觸發使用OnPush更改檢測策略的更改檢測:
setTimeOut
setInterval
- Promise.resolve().then(), (of course, the same for Promise.reject().then())
this.http.get('...').subscribe() (in general, any RxJS observable subscription)
You can test this behavior using the simple demo:
- Click on "Change Age" button in HeroCardOnPushComponent which uses ChangeDetectionStrategy.OnPush
- 驗證觸發了變更檢測並檢查所有元件
Trigger Change Detection Manually 手動觸發變更檢測
存在三種手動觸發更改檢測的方法:
- ChangeDetectorRef的detectChanges()通過牢記更改檢測策略在此檢視及其子級上執行更改檢測。它可以與detach()結合使用以實現本地更改檢測檢查。
- ApplicationRef.tick()通過遵守元件的更改檢測策略來觸發整個應用程式的更改檢測
- ChangeDetectorRef上的markForCheck()不會觸發更改檢測,但會將所有OnPush祖先標記為要檢查一次,作為當前或下一個更改檢測週期的一部分。即使已標記的元件使用OnPush策略,它也將執行更改檢測。
手動執行變更檢測不是黑客,但您只能在合理的情況下使用它,
下圖以可視表示形式顯示了不同的ChangeDetectorRef方法:
您可以在DEMO中使用“ DC”(detectChanges())和“ MFC”(markForCheck())按鈕來測試其中一些操作。
### Async Pipe
內建的AsyncPipe訂閱一個observable並返回它發出的最新值。
每次發出新值時,AsyncPipe內部都會呼叫markForCheck,請參見其原始碼:
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
如圖所示,AsyncPipe使用OnPush更改檢測策略自動執行。因此,建議儘可能使用它,以便以後執行從預設更改檢測策略到OnPush的切換。
您可以在非同步演示中看到這種行為。
第一個元件通過AsyncPipe將可觀察物件直接繫結到模板
<mat-card-title>{{ (hero$ | async).name }}</mat-card-title>
hero$: Observable<Hero>;
ngOnInit(): void {
this.hero$ = interval(1000).pipe(
startWith(createHero()),
map(() => createHero())
);
}
而第二個元件訂閱可觀察物件並更新資料繫結值:
<mat-card-title>{{ hero.name }}</mat-card-title>
hero: Hero = createHero();
ngOnInit(): void {
interval(1000)
.pipe(map(() => createHero()))
.subscribe(() => {
this.hero = createHero();
console.log(
'HeroCardAsyncPipeComponent new hero without AsyncPipe: ',
this.hero
);
});
}
如您所見,沒有AsyncPipe的實現不會觸發更改檢測,因此我們需要為可觀察物件發出的每個新事件手動呼叫detectChanges()
避免變化檢測迴圈和ExpressionChangedAfterCheckedError
Angular包括一種檢測變化檢測迴圈的機制。在開發模式下,框架執行兩次更改檢測,以檢查自第一次執行以來該值是否已更改。在生產模式下,更改檢測僅執行一次即可獲得更好的效能。
我在ExpressionChangedAfterCheckedError演示中強加了該錯誤,如果開啟瀏覽器控制檯,則可以看到它:
在此演示中,我通過更新ngAfterViewInit生命週期掛鉤中的hero屬性來強制執行錯誤:
ngAfterViewInit(): void {
this.hero.name = 'Another name which triggers ExpressionChangedAfterItHasBeenCheckedError';
}
要了解為什麼這會導致錯誤,我們需要檢視更改檢測執行期間的不同步驟:
如我們所見,在呈現了當前檢視的DOM更新之後,將呼叫AfterViewInit生命週期掛鉤。如果我們更改此掛鉤中的值,則它將在第二次更改檢測執行中具有不同的值(如上所述,這是在開發模式下自動觸發的),因此Angular將丟擲ExpressionChangedAfterCheckedError。
我可以強烈推薦Max Koretskyi撰寫的有關Angular中的更改檢測所需的所有知識,它詳細探討了著名的ExpressionChangedAfterCheckedError的基礎實現和用例。
沒有更改檢測的執行程式碼
可以在NgZone外部執行某些程式碼塊,以便它不會觸發更改檢測。
constructor(private ngZone: NgZone) {}
runWithoutChangeDetection() {
this.ngZone.runOutsideAngular(() => {
// the following setTimeout will not trigger change detection
setTimeout(() => doStuff(), 1000);
});
}
這個簡單的演示提供了一個按鈕來觸發Angular區域之外的動作:
您應該看到該操作已記錄在控制檯中,但是HeroCard元件未選中,這意味著它們的邊框不會變成紅色。
此機制對於由量角器執行的E2E測試很有用,特別是如果您在測試中使用browser.waitForAngular。將每個命令傳送到瀏覽器後,量角器將等待,直到區域變得穩定為止。如果使用setInterval,則區域將永遠不會變得穩定,並且測試可能會超時。
RxJS可觀察物件可能發生相同的問題,但是您需要按照Zone.js對非標準API的支援中所述,將修補版本新增到polyfill.ts中:
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js/dist/zone-patch-rxjs'; // Import RxJS patch to make sure RxJS runs in the correct zone
如果沒有此修補程式,則可以在ngZone.runOutsideAngular內部執行可觀察的程式碼,但仍可以作為任務在NgZone內部執行
停用變更檢測
在特殊的使用情況下,有必要停用更改檢測。例如,如果您使用WebSocket將大量資料從後端推送到前端,則相應的前端元件僅應每10秒更新一次。在這種情況下,我們可以通過呼叫detach()來停用更改檢測,並使用detectChanges()手動觸發它:
constructor(private ref: ChangeDetectorRef) {
ref.detach(); // deactivate change detection
setInterval(() => {
this.ref.detectChanges(); // manually trigger change detection
}, 10 * 1000);
}
在Angular應用程式的引導過程中,也可以完全停用Zone.js。這意味著自動更改檢測功能已完全停用,我們需要手動觸發使用者介面更改,例如通過呼叫ChangeDetectorRef.detectChanges()。
首先,我們需要註釋掉從polyfills.ts匯入的Zone.js:
import 'zone.js/dist/zone'; // Included with Angular CLI.
接下來,我們需要在main.ts中傳遞noop區域:
platformBrowserDynamic().bootstrapModule(AppModule, {
ngZone: 'noop';
}).catch(err => console.log(err));
有關停用Zone.js的更多詳細資訊,請參見文章沒有Zone.Js的Angular Elements。
Ivy
從Angular 9開始,Angular預設使用Ivy,它是Angular的下一代編譯和渲染管道。
Ivy仍然以正確的順序處理所有框架生命週期掛鉤,以便更改檢測像以前一樣工作。因此,您仍將在應用程式中看到相同的ExpressionChangedAfterCheckedError。
Max Koretskyi在文章中寫道:
如您所見,所有熟悉的操作仍在這裡。但是操作順序似乎已經改變。例如,現在看來Angular首先檢查子元件,然後才檢查嵌入式檢視。由於目前沒有編譯器可以生成適合於檢驗我的假設的輸出,因此我不確定。
您可以在此博文末尾的“推薦文章”部分中找到另外兩個與Ivy相關的有趣文章。
最後
Angular Change Detection是一種強大的框架機制,可確保我們的UI以可預測和高效的方式表示我們的資料。可以肯定地說,更改檢測僅適用於大多數應用程式,尤其是當它們不包含50多個元件時。
作為開發人員,您通常需要深入探討此主題,原因有兩個:
- 您收到一個ExpressionChangedAfterCheckedError並需要解決它
- 您需要提高應用程式效能
我希望本文可以幫助您更好地瞭解Angular的變更檢測。隨意使用我的演示專案來試用不同的變更檢測策略。
推薦文章
- Angular Change Detection - How Does It Really Work?
- Angular OnPush Change Detection and Component Design - Avoid Common Pitfalls
- A Comprehensive Guide to Angular onPush Change Detection Strategy
- Angular Change Detection Explained
- Angular Ivy change detection execution: are you prepared?
- Understanding Angular Ivy: Incremental DOM and Virtual DOM