[譯] 關於Angular的變動檢測(Change Detection)你須要知道這些

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

若是你像我同樣,想對Angular的變動檢測機制有一個深刻的理解,因爲在網上並無多少有用的信息,你只能去看源碼。大多數文章都會提到每個組件都會有一個屬於本身的變動檢測器(change detector),它負責檢查和這個組件,可是他們幾乎都僅限於在說怎麼使用immutable 數據和變動檢測策略,這篇文章將會讓你明白爲何使用immutable能夠工做,而且髒檢查機制是如何影響檢查的過程的。還有,這篇文章將會引起你對性能優化方面的一些場景的思考。git

這篇文章包含2部分,第一部至關的有技術含量,它包含了一些指向源碼的連接,它詳細的介紹了髒檢查機制在Angular的底層是怎麼運行的,全部內容是基·Angular的最新 版本-4.0.1(注:做者寫這篇文章的時候,Angular的最新版本是4.0.1), 髒檢查機制的實如今這個版本的實現和以前的2.4.1版本是不同的,若是你對以前版本的實現感興趣的話,你能夠在這個stackoverflow的答案上學習到一些東西。github

第二部分介紹了變動檢測在應用程序中該怎麼使用,這部份內容既適用於以前的2.4.1版本,也使用於最新的4.0.1版本,由於這部分的API並無改變。性能優化

將視圖(view)做爲一個核心概念

在Angular的教程中提到過,一個Angular應用程序就是一個組件樹,然而,Angular在底層用了一個低級的抽象,叫作 視圖(view)。一個視圖和一個組件之間有直接的關聯:一個視圖對應着一個組件,反之亦然。一個視圖經過一個叫component的屬性,保持着對與其所關聯的那個組件類的實例的引用。全部的操做(好比屬性檢查,DOM更新等),都會表如今視圖上面,所以從技術上來說,更正確的說法是,Angular是一個視圖樹,一個組件能夠被看作是一個視圖的更高級的概念。下面是一些源碼中的關於視圖的介紹.bash

一個視圖是一個應用程序UI的基本組成單位,它是可以被一塊兒建立和銷燬的最小的一個元素集合。
在一個視圖中,元素的屬性能夠改變,可是它的結構(數量和順序)不會被改變,只有經過一個ViewContainerRef來插入、移動或是刪除內嵌的視圖這些操做才能夠改變元素的結構。每個視圖能夠包含多個視圖容器。markdown

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

在這裏有一點須要注意的是,網上的全部文章和StackOverflow上的一些回答將變動檢測視爲變動檢測器對象或者`ChangeDetectorRef`,指的就是我在這裏所說的視圖(view)。實際上,沒有一個單獨的對象來進行變動檢測,而且視圖纔是變動檢測所運行的地方。
複製代碼

每個視圖通nodes屬性對它的子視圖有一個引用,所以,它能夠在它的子視圖中執行一些操做。異步

視圖狀態(View state)

每個視圖都有一個狀態,它扮演着很是重要的角色,由於根據這個狀態的值,Angular來決定是要對這個視圖以及它的子視圖進行變動檢測仍是忽略掉。有許多可能的狀態,可是下面的這幾個是與本文相關的幾個。ide

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

若是ChecksEnabled是false或者視圖是Errored或者Destroyed的狀態,變動檢測將會跳過這個視圖以及它的子視圖。默認的,全部的視圖都被初始化爲ChecksEnabled的狀態,除非你設置了ChangeDetectionStrategy.OnPush。稍後將會詳細介紹。視圖的狀態也能夠合併,例如,一個視圖既能夠有FirstCheck的狀態,也能夠由ChecksEnabled的狀態。函數

Angular有許多高級的概念來操做視圖,我在這裏寫了一些,其中一個就是viewRef,它封裝了基本的組件視圖,還有一個指定的方法detectChanges,當一個異步事件發生的時候,Angular將會在它的頂級viewRef觸發變動檢測,它會在對它本身進行變動檢測後對它的子視圖進行變動檢測。

你能夠經過ChangeDetectorRef標記將這個viewRef注入到一個組件的constructor中:

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. 調用子組件的OnInitngDoCheck生命週期鉤子(OnInit只有在第一次檢查的時候纔會被調用)。

  7. 在子視圖組件實例中更新ContentChildren queryList

  8. 在子組件實例中調用AfterContentInitAfterContentChecked生命週期鉤子(AfterContentInit只有在第一次檢查的時候纔會被調用)。

  9. 若是當前視圖組件實例上的屬性變化的話,更新DOM插值表達式。

  10. 對子視圖執行變動檢查(重複這個列表裏的步驟)。

  11. 更新當前視圖組件實例中的ViewChildren查詢列表。

  12. 在當前組件實例中調用AfterViewInitAfterViewChecked生命週期鉤子(AfterViewInit只有在第一次檢查的時候纔會被調用)。

  13. 禁用當前視圖的檢查(一部分是變動檢測策略的實現)。

基於上面的執行列表,有幾個須要強調的事情。

第一個事情就是onChanges生命週期鉤子是發生在子組件中的,它在子視圖被檢查以前觸發的,而且即便這個子視圖沒有進行變動檢測它也會觸發。這是個很重要的信息,本文的第二部分你將會看到咱們怎麼利用這個信息。

第二個事情就是當視圖被檢測的時候,它的DOM的更新是做爲變動檢測機制的一部分的,也就是說若是一個組件沒有被檢查,即便這個組件的被用到模板上的屬性改變了,DOM也不會被更新。模板是在第一次檢查前就被渲染了,我所指的DOM更新實際上指的是插值表達式的更新,所以若是你有一個這樣的模板<span>some {{name}}</span>,DOM元素span將會在第一次檢查前就被渲染,而在檢查的時候,只有{{name}}這部分纔會被渲染。

另一個有趣的發現是在變動檢測期間,一個子組件的視圖的狀態會被改變。我在前面提到過全部的組件視圖在初始化時默認都是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)。這是檢查子組件視圖狀態的地方,若是ChecksEnabledtrue,那麼執行變動檢測,下面是相關的代碼:

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

如今你已經知道了視圖的狀態控制着是否要對這個視圖以及它的子組件執行變動檢測,因此問題是咱們能控制這些狀態碼?答案是能夠,這也是本文第二部分要講的內容。

有的聲明週期鉤子在DOM更新以前被調用(3,4,5),有的是在以後(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
複製代碼

探索含義(Exploring the implications)

咱們假設有下面的一個組件樹:

正如咱們上面所學到的,每個組件都有一個與之相關聯的組件視圖,每個視圖初始化時的ViewState.ChecksEnabled都爲true,這就意味着當Angular執行變動檢測時,組件樹上的每個組件杜輝被檢查。

假設咱們想禁用掉AComponent及它的子組件的變動檢測,咱們只須要很簡單的把它的ViewState.ChecksEnabled設置爲false就能夠的。直接改變狀態是一個低級的操做,所以Angular爲咱們提供了一些在視圖上可用的公共方法。每個組件均可以經過ChangeDetectorRef來得到與其關聯的視圖的引用,Angular文檔中爲這個類定義了以下的公共接口:

class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}
複製代碼

讓咱們看看咱們看以從中收穫點什麼吧。

deatch

第一個咱們能夠操做視圖的方法是deatch,它僅僅是可以禁用掉對當前視圖的檢查:

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

複製代碼

讓咱們看看怎麼在代碼中使用它:

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

它確保了在接下來的變動檢測中,以AComponent爲開始的左側部分將會被忽略掉(橘黃色的組件將不會被檢查):

在這裏有兩個地方須要注意--第一個就是就是咱們改變了AComponent的檢測狀態,全部它的子組件也不會被檢查。第二個就是因爲左側的組件們北郵執行變動檢測,全部他們呢的模板視圖也不會被更新,下面是一個小例子來證實這一點:

@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);
  }
複製代碼

第一次(檢查)的時候,span標籤將會被渲染成文本See if I change: false. 當2秒後,changed屬性變爲true的時候,span標籤中的文本將不會改變,但當咱們刪掉this.cd.detach()的時候,一切都會如期執行。

reattach

像本文中第一部分中所說的那樣,若是綁定的輸入屬性aPropAppComponent中改變了,AComponentOnChanges生命週期鉤子仍舊會觸發。這就意味着一旦咱們輸入屬性改變了,咱們就能夠激活當前視圖的變動檢測器去執行變動檢測,而後在下個事件循環中再把它從deatch(變動檢測樹中分離)掉,下面的代碼片斷證實了這一點:

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;
}
複製代碼

從上面的實現中能夠看到,它僅僅是向上遍歷,對全部的父組件啓用檢查一直到根組件。

何時它是有用的呢?就像是ngOnChanges同樣,即便組件使用OnPush策略,ngDoCheck生命週期鉤子也會被觸發,一樣的,只有在禁用分支的最頂層的組件中才會被觸發,而不是禁用分支的全部組件。可是咱們能夠用這個鉤子來執行一些定製化的邏輯,使咱們的組件能夠在一個變動檢測週期中執行檢查。因爲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();
  }
複製代碼

當輸入屬性改變的時候,即便變動檢測器還保持着分離的狀態,DOM也會更新。

checkNoChanges

變動檢測器上最後一個有用的方法是在運行當前的變動檢測時,確保沒有變化發生。基本上,它執行了本文第一部分那個步驟中的1,7,8的操做,而且當它發現一個綁定值變化了或是決定DOM應該要被更行的時候,將會拋出一個異常。

相關文章
相關標籤/搜索