[譯]關於Angular髒值檢查你應該知道的最新指南

原文javascript

Angular髒值檢查

image

本文提供了您須要瞭解的有關變動檢測的全部必要信息。經過使用本文構建的演示項目來解釋angular 的變動檢測機制。

Angular的變動檢測是該框架的核心機制,但(至少以個人經驗)很難理解。更不幸的是,官方網站上沒有關於此主題的官方指南。html

What Is Change Detection

Angular的兩個主要目標是可預測和高效。框架須要經過組合狀態和模板來在UI上覆制應用程序的狀態:前端

image

若是狀態發生任何更改,也必須更新DOM。將HTML與咱們的數據同步的機制稱爲「更改檢測」。每一個前端框架都使用其實現,例如React使用虛擬DOM,Angular使用更改檢測等等。我能夠推薦文章「 JavaScript框架中的更改及其檢測」,該文章很好地概述了此主題。java

更改檢測:數據更改後更新DOM的過程

做爲開發人員,大多數時候咱們不須要關心變動檢測,除非咱們須要優化應用程序的性能。若是處理不當,更改檢測會下降大型應用程序的性能。npm

How Change Detection Works 變動檢測是如何工做的

變動檢測週期能夠分爲兩個部分:bootstrap

  • 開發人員更新應用程序模型
  • Angular經過從新渲染來同步DOM中的更新模型

讓咱們更詳細地看一下這個過程:後端

  1. 開發人員更新數據模型,例如經過更新組件綁定
  2. angular 檢測變化
  3. 變動檢測從上到下檢查組件樹中的每一個組件,以查看相應的模型是否已更改
  4. 若是有新值,它將更新組件的視圖(DOM)

    如下GIF以簡化的方式演示了此過程:數組

    image

該圖顯示了Angular組件樹及其在應用程序引導過程當中爲每一個組件建立的更改檢測器(CD)。該檢測器將當前值與屬性的先前值進行比較。若是該值已更改,它將==isChanged==設置爲==true==。檢查框架代碼中的實現,這只是與NaN的特殊處理進行的===比較。promise

Zone.js

通常狀況下,zone能夠跟蹤並攔截任何異步任務。瀏覽器

Zone 一般具備如下階段:

  • 開始穩定
  • 若是任務在區域中運行,它將變得不穩定,
  • 若是任務完成,它將再次變得穩定

Angular在啓動時修補了幾個低級瀏覽器API,以便可以檢測到應用程序中的更改。這是使用zone.js完成的,該區域修補了EventEmitter,DOM事件偵聽器,XMLHttpRequest,Node.js中的fs API等API。

簡而言之,若是發生如下事件之一,則框架將觸發更改檢測

  • 任何瀏覽器事件(單擊,鍵入等)
  • setInterval() and setTimeout()
  • 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等)時,此默認策略都會從上到下檢查組件樹中的每一個組件。這種不對組件的依賴項作任何假設的保守檢查方法稱爲髒檢查。它可能會對包含許多組件的大型應用程序的性能產生負面影響。

image

OnPush Change Detection Strategy

經過將changeDetection屬性添加到組件裝飾器元數據中,咱們能夠切換到ChangeDetectionStrategy.OnPush更改檢測策略:

@Component({
    selector: 'hero-card',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class HeroCard {
    ...
}

這種更改檢測策略能夠跳過對此組件及其全部子組件的沒必要要檢查。
下一個GIF演示了使用OnPush更改檢測策略跳過組件樹的各個部分:
image

使用此策略,Angular知道僅在如下狀況下才須要更新組件:

  • 輸入屬性已更改, 標記爲@Input() 的屬性;
  • 該組件或其子組件之一觸發事件處理程序
  • 手動觸發變化檢測
  • 經過異步管道連接到模板的可觀察對象發出新值, 如 data | async

    讓咱們仔細看看這些事件類型。

    Input Reference Changes

    在默認的更改檢測策略中,每當@Input()數據被更改或修改時,Angular將運行更改檢測器。使用OnPush策略,僅當新引用做爲@Input()值傳遞時,纔會觸發更改檢測器。

    JavaScript中的全部內容都是按引用傳遞的,可是全部基元都是不可變的,而且它們的文字表示均指向相同的基元實例/引用。修改對象屬性或數組條目不會建立新引用,所以不會觸發OnPush組件上的更改檢測。要觸發變動檢測器,您須要傳遞一個新的對象或數組引用。

您可使用簡單DEMO測試此行爲:

  1. 使用ChangeDetectionStrategy.Default修改HeroCardComponent的 age
  2. 驗證帶有ChangeDetectionStrategy.OnPush的HeroCardOnPushComponent不能反映更改的age(經過組件周圍的紅色邊框顯示)
  3. 在「修改英雄」面板中單擊「建立新對象引用」
  4. 驗證是否經過更改檢測檢查了具備ChangeDetectionStrategy.OnPush的HeroCardOnPushComponent

image

爲防止更改檢測錯誤,在全部地方僅使用不可變的對象和列表使用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:

    1. Click on "Change Age" button in HeroCardOnPushComponent which uses ChangeDetectionStrategy.OnPush
    2. 驗證觸發了變動檢測並檢查全部組件

image

Trigger Change Detection Manually 手動觸發變動檢測

存在三種手動觸發更改檢測的方法:

  • ChangeDetectorRef的detectChanges()經過牢記更改檢測策略在此視圖及其子級上運行更改檢測。它能夠與detach()結合使用以實現本地更改檢測檢查。
  • ApplicationRef.tick()經過遵照組件的更改檢測策略來觸發整個應用程序的更改檢測
  • ChangeDetectorRef上的markForCheck()不會觸發更改檢測,但會將全部OnPush祖先標記爲要檢查一次,做爲當前或下一個更改檢測週期的一部分。即便已標記的組件使用OnPush策略,它也將運行更改檢測。
手動運行變動檢測不是黑客,但您只能在合理的狀況下使用它,

下圖以可視表示形式顯示了不一樣的ChangeDetectorRef方法:

image

您能夠在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的切換。

您能夠在異步演示中看到這種行爲。

image

第一個組件經過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演示中強加了該錯誤,若是打開瀏覽器控制檯,則能夠看到它:

image

在此演示中,我經過更新ngAfterViewInit生命週期掛鉤中的hero屬性來強制執行錯誤:

ngAfterViewInit(): void {
    this.hero.name = 'Another name which triggers ExpressionChangedAfterItHasBeenCheckedError';
  }

要了解爲何這會致使錯誤,咱們須要查看更改檢測運行期間的不一樣步驟:

image

如咱們所見,在呈現了當前視圖的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區域以外的動做:

image

您應該看到該操做已記錄在控制檯中,可是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的變動檢測。隨意使用個人演示項目來試用不一樣的變動檢測策略。

推薦文章

相關文章
相關標籤/搜索