你所要知道的全部關於Angular的變化檢測機制

原文地址:Everything you need to know about change detection in Angularhtml

若是想像我同樣全面的瞭解Angular的髒值檢測機制,除了瀏覽源代碼以外別無他法,網上可沒有太多可用信息。大部分文章都提到,Angular中每一個組件都自帶一個髒值檢測器,可是它們都僅僅停留在髒值檢測的策略和案例的使用,並無作太多的深刻。本篇文章將帶你瞭解案例背後的故事以及髒值檢測策略的影響,此外,掌握本章內容後你將可以獨立提出各類性能提高的解決方案。git

文章包含兩部分,第一部分的內容基於Angular 4.0.1,內容偏技術性會包含不少源碼連接,解釋了髒值檢測的深層機制,第二部分展現了髒值檢測在具體應用中的使用(譯者注1)。github

視圖(views)是核心概念

教程中有提到:Angular 應用是由組件樹構成的,然而,angular在底層實現中使用視圖做爲其更低層次的抽象。組件和視圖是有直接聯繫的,一個視圖對應一個組件,反之亦然。視圖擁有一個component屬性,它是組件實例的引用。全部的操做(好比屬性的檢測、DOM的更新)都在視圖層面上完成,所以準確的說Angular應該是由視圖樹構成的,組件能夠被描述爲視圖的更高級概念。你能夠在這裏查看相關源碼。dom

視圖是應用UI的基本構建,它也是建立和銷燬元素的最小組合。

視圖中元素的屬性能夠直接更改,但元素的結構(數量和順序)不能,只能經過ViewContainerRef插入,移動或移除嵌套視圖來更改元素的結構。每一個視圖能夠包含許多視圖容器。異步

在本文中,我將交替使用組件視圖和組件的概念。ide

須要注意的是:全部關於髒值檢測的文章和 StackOverflow上的答案都把我上面提到的視圖(View)做爲髒值檢測的對象或者ChangeDetectorRef,但實際上,髒值檢測沒有一個專門的對象。

每一個視圖能夠經過節點屬性連接子視圖,所以能夠對子視圖執行操做。函數

屬性的狀態

每一個視圖都有一個狀態,它扮演着很是重要的角色,由於根據它的值,Angular決定是否對視圖及其全部子項運行或跳過髒值檢測。狀態的值可能有不少個,但如下是與本文相關的:性能

  • 1.FirstCheck
  • 2.ChecksEnabled
  • 3.Errored
  • 4.Destroyed

若是ChecksEnabledfalse或view的狀態值是ErroredDestroyed,則視圖及其子視圖將跳過髒值檢測。除非髒值檢測的策略( ChangeDetectionStrategy)是OnPush,不然默認狀況下,全部視圖的初始化狀態都會是ChecksEnabled 。狀態是能夠並存的,例如,一個視圖能夠同時有FirstCheckChecksEnabled兩個狀態。this

Angular有一系列高級的概念來操縱視圖。我在這裏寫了一些關於它們的文章。ViewRef就是其中的一個。它封裝了底層組件視圖,而且有一個恰當命名的方法detectChanges。當異步事件發生時,Angular 會在其最頂層的ViewRef上觸發髒值檢測,在自身運行完畢後,它會爲其子視圖執行髒值檢測。spa

你可使用ChangeDetectorRef在組件的構造函數中注入viewRef:

export class AppComponent {
    constructor(cd: ChangeDetectorRef) { ... }

你能夠從這個類的定義中看出端倪:

export declare abstract class ChangeDetectorRef {
        abstract checkNoChanges(): void;
        abstract detach(): void;
        abstract detectChanges(): void;
        abstract markForCheck(): void;
        abstract reattach(): void;
    }


export abstract class ViewRef extends ChangeDetectorRef {
  ...
}

髒值檢測的順序

負責視圖髒值檢測的主要邏輯存在於checkAndUpdateView函數中。其大部分功能都集中在子組件視圖的操做上。從宿主組件開始爲每一個組件遞歸調用該函數。這意味着隨着遞歸樹的展開子組件在下一次調用時變成父組件。

對於某一特定的視圖,該函數按照以下順序執行操做:

  1. 若是視圖是第一次被檢測,那麼 ViewState.firstCheck設置爲true,不然爲false
  2. 檢查和更新子組件/指令實例上的輸入屬性
  3. 更新子視圖髒值檢測狀態
  4. 運行嵌入視圖的髒值檢測(重複列表中步驟)
  5. 若是綁定的輸入值發生變化,子組件調用OnChanges這個生命週期鉤子
  6. 在子組件上調用OnInit和ngDoCheck(OnInit僅在第一次檢查時調用 )
  7. 子組件更新 ContentChildren查詢列表
  8. 在子組件上調用AfterContentInit和AfterContentChecked(AfterContentInit僅在第一次檢查時調用 )
  9. 若是當前視圖組件實例的屬性有改變則更新對應的DOM插值
  10. 爲子視圖運行髒值檢測(重複列表中步驟)
  11. 更新當前視圖的 ViewChildren 查詢列表
  12. 子組件上調用AfterViewInit和AfterViewChecked生命週期鉤子(AfterViewInit僅在第一次檢查時調用)
  13. 更新視圖檢測狀態爲禁用

在這裏有必要強調幾件事:

1.在檢查子視圖以前,Angular會先觸發子組件的onChanges ,即便子視圖不進行髒值檢測,onChanges也會被觸發。這一條很重要,咱們將在文章的第二部分看到如何利用這些知識。

2.視圖的DOM更新是做爲髒值檢測機制的一部分存在的,也就是說若是組件沒有被檢測,即便模板中使用的組件屬性發生更改,DOM也不會更新(我這裏提到的DOM更新其實是插值表達式的更新。 )。模板會在首次檢測以前完成渲染,舉個例子,對於 <span>some {{name}}</span>這個html,DOM元素 span 會在第一次檢測前就渲染完,在檢測期間,只有 {{name}} 會被渲染。

3.另外一個觀察到的有趣現象是:在髒值檢測期間,子組件視圖的狀態是能夠被改變的。我在前面提到,在默認狀況下,全部的組件的狀態都會初始化 ChecksEnabled ,可是對於使用 OnPush 這個策略的組件來講,髒值檢測機制會在第一次後被禁用(操做步驟9)

if (view.def.flags & ViewFlags.OnPush) {
  view.state &= ~ViewState.ChecksEnabled;
}

這意味着在接下來的髒值檢測運行期間,該組件視圖及其全部子組件將會跳過該檢查。有關OnPush策略的文檔指出,只有在組件的綁定發生變化時纔會檢查該組件。因此要作到這一點,必須經過設置ChecksEnabled來啓用檢查。這就是下面的代碼所作的(操做2):

if (compView.def.flags & ViewFlags.OnPush) {
  compView.state |= ViewState.ChecksEnabled;
}

僅當父級視圖的綁定發生變化且子組件視圖的髒值檢測策略已使用初始化爲ChangeDetectionStrategy.OnPush,狀態纔會更新

最後,當前視圖的髒值檢測負責啓動子視圖的檢測(操做8)。若是是視圖狀態是ChecksEnabled,則對此視圖執行更改檢測。這裏是相關的代碼:

viewState = view.state;
...
case ViewAction.CheckAndUpdate:
  if ((viewState & ViewState.ChecksEnabled) &&
    (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) {
    checkAndUpdateView(view);
  }
}

如今你知道視圖及其子視圖是否運行髒值檢測是由視圖狀態控制的。那麼咱們能夠控制視圖的狀態嗎?事實證實,咱們能夠,這是本文第二部分須要討論的。

一些生命週期的鉤子(步驟3,4,5)是在DOM更新前被調用的,另外一些則是以後運行(操做9)。若是咱們有以下組件層級:A->B->C,下面是鉤子回調和綁定更新的順序:

A: AfterContentInit
A: AfterContentChecked
A: Update bindings
    B: AfterContentInit
    B: AfterContentChecked
    B: Update bindings
        C: AfterContentInit
        C: AfterContentChecked
        C: Update bindings
        C: AfterViewInit
        C: AfterViewChecked
    B: AfterViewInit
    B: AfterViewChecked
A: AfterViewInit
A: AfterViewChecked

探索影響

讓咱們假設有以下組件樹:

圖片描述

咱們知道,一個組件對應一個視圖。每一個視圖的狀態都被初始化爲 ViewState.ChecksEnabled,也就意味着在組件樹上的每個組件都將運行髒值檢測。

假設咱們想要禁用AComponent及其子項的髒值檢測,經過設置 ViewState.ChecksEnabledfalse是最簡答的方式。可是直接改變狀態在Angular中是底層操做,爲此Angular提供了一些列公開方法。每一個組件能夠經過 ChangeDetectorRef標識來獲取關聯視圖。 Angular文檔定義瞭如下公共接口:

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

  detectChanges() : void
  checkNoChanges() : void
}

讓咱們看看這能夠爲咱們帶來什麼好處。

detach

第一種容許咱們操做狀態的方法是detach,它能夠簡單地禁用對當前視圖的髒值檢測:

detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }

讓咱們看看它在代碼中的應用:

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

如今能夠確保在髒值檢測運行期間,左側分支(從AComponent開始,橘色部分)AComponent將跳過檢測:

這裏須要注意兩點:第一點,即便咱們改變了ACompoent的狀態,它的全部子組件也不會進行檢測;第二點,隨着左側分支髒值檢測的中止,DOM更新也再也不運行。這裏舉個小例子:

@Component({
  selector: 'a-comp',
  template: `<span>See if I change: {{changed}}</span>`
})
export class AComponent {
  constructor(public cd: ChangeDetectorRef) {
    this.changed = 'false';

    setTimeout(() => {
      this.cd.detach();
      this.changed = 'true';
    }, 2000);
  }

一開始模板會被渲染成 See if I change: false,兩秒以後change這個屬性的值變爲true,但相對應的文字卻沒有改過來。若是咱們刪除了this.cd.detach(),一切就會如期進行。

reattach

正如文章第一部分所述,若是輸入屬性發生變化,OnChanges就會被觸發。這意味着一旦咱們知曉輸入屬性了變化,咱們就能夠激活當前組件的檢測器來運行髒值檢測,並在下一輪關閉它。舉個例子:

export class AComponent {
  @Input() inputAProp;

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

  ngOnChanges(values) {
    this.cd.reattach();
    setTimeout(() => {
      this.cd.detach();
    })
  }

reattach經過位運算簡單的設置了 ViewState.ChecksEnabled

reattach(): void { this._view.state |= ViewState.ChecksEnabled; }

這幾乎等同於把ChangeDetectionStrategy設置爲OnPush:在第一次更改檢測運行後禁用檢查,在父組件綁定屬性變化時啓用它,並在運行後禁用。

請注意,OnChanges僅在禁用分支中最頂層的組件中觸發,而不是在禁用的分支中的每一個組件觸發。

markForCheck

reattach方法僅做用於當前組件,對父級組件則不起做用。這意味着該reattach方法僅適用於禁用分支中最頂層的組件。

咱們須要一種啓用從當前組件到根組件檢測的方法,markForCheck應用而生:

let currView: ViewData|null = view;
while (currView) {
  if (currView.def.flags & ViewFlags.OnPush) {
    currView.state |= ViewState.ChecksEnabled;
  }
  currView = currView.viewContainerParent || currView.parent;
}

從源碼的實現中咱們能夠看到,markForCheck向上逐層遍歷並啓用檢測。

這有什麼用處呢?正如在檢測策略爲OnPush的狀況下, ngOnChangesngDoCheck依舊能夠被觸發, 一樣,它僅在被禁用分支中的最頂層組件觸發,而不是被禁用分支中的每一個組件觸發。 可是咱們可使用該鉤子來執行自定義邏輯,並將咱們的組件標記爲符合一次髒值檢測週期運行。 因爲Angular只檢查對象引用,因此咱們能夠實現一些對象屬性的髒檢查:

Component({
  ...,
  changeDetection: ChangeDetectionStrategy.OnPush
})
MyComponent {
  @Input() items;
  prevLength;
  constructor(cd: ChangeDetectorRef) {}

  ngOnInit() {
      this.prevLength = this.items.length;
  }

  ngDoCheck() {
      if (this.items.length !== this.prevLength) {
        this.cd.markForCheck();
        this.prevLenght = this.items.length;
      }
  }

detectChanges

使用detectChanges能夠爲當前組件及其全部子項運行一次髒值檢測。此方法會忽略視圖的狀態,這意味着當前視圖可能依舊保持禁用狀態,而且不會對組件進行常規髒值檢測。舉個例子:

export class AComponent {
  @Input() inputAProp;

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

  ngOnChanges(values) {
    this.cd.detectChanges();
  }

即便髒值檢測器依舊是detached,輸入屬性更改時DOM也會更新。

checkNoChanges

髒值檢測的最後一個可用方法是確保在當前檢測運行過程當中不會有變化發生。基本上,它執行了列表中1,7,8操做,若是它發現了須要變動的綁定或者會引起DOM的更新,它都會拋出異常。

譯者注:

1.髒值檢測的底層實如今Angualr不一樣版本有些不一樣,本文第一部分基於Angular4.0.1,若是想了解Angular2.4.1的實現機制,請移步stackoverflow

相關文章
相關標籤/搜索