[Angular][translate]有關Angular的變動檢測

原文連接: https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206fcss

這篇文章只是我本身一邊看原文一邊隨手翻譯的,箇中錯誤以及表述多有不正確,閱讀原文才是最佳選擇。node

若是你也像我同樣想對Angular的變動檢測有更深刻的瞭解,去看源碼無疑是必須的選擇,由於網絡上相關的信息實在是太少了。大多數文章都只是指出每一個組件都有本身的變動檢測器,可是並無繼續深刻,他們大多關注於不可變對象和變動檢測策略的使用。因此這篇文章的目的是告訴你,爲何使用不可變對象會有效?變動檢測策略是如何影響到檢測的?同時,這篇文章也能讓你能對不一樣場景下提出相應的性能優化方法。git

這篇文章由兩部分組成,第一部分技術性較強且有不少源碼部分的引用。主要解釋了變動檢測在底層的工做細節。基於Angular-4.0.1。須要注意的是,2.4.1以後版本的變動檢測策略有較大的變化。若是你對之前版本的變動檢測有興趣,能夠閱讀這篇回答github

第二部分主要講解了如何在應用中使用變動檢測,這部分對於Angular2+都是相同的。由於Angular的公共API並無發生變化。typescript

核心概念-視圖View

Angular的文檔中通篇都提到了一個Angular應用是一個組件樹。可是Angular底層其實使用了一個低級抽象-視圖View。視圖View和組件之間的關係很直接-一個視圖與一個組件相關聯,反之亦然。每一個視圖都在它的component屬性中保持了一個與之關聯的組件實例的引用。全部的相似於屬性檢測、DOM更新之類的操做都是在視圖上進行的。所以,技術上而言把Angular應用描述成一個視圖樹更加準確,由於組件是視圖的一個高階描述。在源碼中有關視圖是這麼描述的:數組

A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.性能優化

視圖是組成應用界面的最小單元,它是一系列元素的組合,一塊兒被建立,一塊兒被銷燬。網絡

Properties of elements in a View can change, but the structure (number and order) of elements in a View cannot. Changing the structure of Elements can only be done by inserting, moving or removing nested Views via a ViewContainerRef. Each View can contain many View Containers.app

視圖中元素的屬性能夠發生變化,可是視圖中元素的數量和順序不能變化。若是想要改變的話,須要經過VireContainerRef來執行插入,移動和刪除操做。每一個視圖都會包括多個View Container。異步

在這篇文章中,組件和組件視圖的概念是互相可替代的。

須要注意的是:網絡上不少文章都把咱們這裏所描述的視圖做爲了變動檢測對象或者ChangeDetectorRef。事實上,Angular中並無一個單獨的對象用來作變動檢測,全部的變動檢測都在視圖上直接運行。

export interface ViewData {
  def: ViewDefinition;
  root: RootData;
  renderer: Renderer2;
  // index of component provider / anchor.
  parentNodeDef: NodeDef|null;
  parent: ViewData|null;
  viewContainerParent: ViewData|null;
  component: any;
  context: any;
  // Attention: Never loop over this, as this will
  // create a polymorphic usage site.
  // Instead: Always loop over ViewDefinition.nodes,
  // and call the right accessor (e.g. `elementData`) based on
  // the NodeType.
  nodes: {[key: number]: NodeData};
  state: ViewState;
  oldValues: any[];
  disposables: DisposableFn[]|null;
}
複製代碼

視圖的狀態

每一個視圖都有本身的狀態,基於這些狀態的值,Angular會決定是否對這個視圖和他全部的子視圖運行變動檢測。視圖有不少狀態值,可是在這篇文章中,下面四個狀態值最爲重要:

// Bitmask of states
export const enum ViewState {
  FirstCheck = 1 << 0,
  ChecksEnabled = 1 << 1,
  Errored = 1 << 2,
  Destroyed = 1 << 3
}
複製代碼

若是CheckedEnabled值爲false或者視圖處於Errored或者Destroyed狀態時,這個視圖的變動檢測就不會執行。默認狀況下,全部視圖初始化時都會帶上CheckEnabled,除非使用了ChangeDetectionStrategy.onPush。有關onPush咱們稍後再講。這些狀態也能夠被合併使用,好比一個視圖能夠同時有FirstCheck和CheckEnabled兩個成員。

針對操做視圖,Angular中有一些封裝出的高級概念,詳見這裏。一個概念是ViewRef。他的_view屬性囊括了組件視圖,同時它還有一個方法detectChanges。當一個異步事件觸發時,Angular從他的最頂層的ViewRef開始觸發變動檢測,而後對子視圖繼續進行變動檢測。

ChangeDectionRef能夠被注入到組件的構造函數中。這個類的定義以下:

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 {
    /** * Destroys the view and all of the data structures associated with it. */
    abstract destroy(): void;
    abstract get destroyed(): boolean;
    abstract onDestroy(callback: Function): any
}
複製代碼

變動檢測操做

負責對視圖運行變動檢測的主要邏輯屬於checkAndUpdateView方法。他的大部分功能都是對子組件視圖進行操做。從宿主組件開始,這個方法被遞歸調用做用於每個組件。這意味着當遞歸樹展開時,在下一次調用這個方法時子組件會成爲父組件。

當在某個特定視圖上開始觸發這個方法時,如下操做會依次發生:

  1. 若是這是視圖的第一次檢測,將ViewState.firstCheck設置爲true,不然爲false;
  2. 檢查並更新子組件/指令的輸入屬性-checkAndUpdateDirectiveInline
  3. 更新子視圖的變動檢測狀態(屬於變動檢測策略實現的一部分)
  4. 對內嵌視圖運行變動檢測(重複列表中的步驟)
  5. 若是綁定的值發生變化,調用子組件的onChanges生命週期鉤子;
  6. 調用子組件的OnInit和DoCheck兩個生命週期鉤子(OnInit只在第一次變動檢測時調用)
  7. 在子組件視圖上更新ContentChildren列表-checkAndUpdateQuery
  8. 調用子組件的AfterContentInit和AfterContentChecked(前者只在第一次檢測時調用)-callProviderLifecycles
  9. 若是當前視圖組件上的屬性發生變化,更新DOM
  10. 對子視圖執行變動檢測-callViewAction
  11. 更新當前視圖組件的ViewChildren列表-checkAndUpdateQuery
  12. 調用子組件的AfterViewInit和AfterViewChecked-callProviderLifecycles
  13. 對當前視圖禁用檢測

在以上操做中有幾點須要注意

深刻這些操做的含義

假設咱們如今有一棵組件樹:

在上面的講解中咱們得知了每一個組件都和一個組件視圖相關聯。每一個視圖都使用ViewState.checksEnabled初始化了。這意味着當Angular開始變動檢測時,整棵組件樹上的全部組件都會被檢測;

假設此時咱們須要禁用AComponent和它的子組件的變動檢測,咱們只要將它的ViewState.checksEnabled設置爲false就行。這聽起來很容易,可是改變state的值是一個很底層的操做,所以Angular在視圖上提供了不少方法。經過ChangeDetectorRef每一個組件能夠得到與之關聯的視圖。

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

detach

這個方法簡單的禁止了對當前視圖的檢測;

detach(): void {
    this._view.state &= ~ViewState.checksEnabled;
}
複製代碼

在組件中的使用方法:

export class AComponent {
    constructor( private cd: ChangeDectectorRef, ) {
        this.cd.detach();
    }
}
複製代碼

這樣就會致使在接下來的變動檢測中AComponent及子組件都會被跳過。

這裏有兩點須要注意:

  • 雖然咱們只修改了AComponent的state值,可是他的子組件也不會被執行變動檢測;
  • 因爲AComponent及其子組件不會有變動檢測,所以他們的DOM也不會有任何更新

下面是一個簡單示例,點擊按鈕後在輸入框中修改就不再會引發下面的p標籤的變化,外部父組件傳遞進來的值發生變化也不會觸發變動檢測:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
@Component({
    selector: 'app-change-dection',
    template: ` <input [(ngModel)]="name"> <button (click)="stopCheck()">中止檢測</button> <p>{{name}}</p> `,
    styleUrls: ['./change-dection.component.css']
})
export class ChangeDectionComponent implements OnInit {
    name = 'erik';
    constructor( private cd: ChangeDetectorRef, ) { }
    ngOnInit() {
    }
    stopCheck() {
        this.cd.detach();
    }
}
複製代碼

reattach

文章第一部分提到:若是AComponent的輸入屬性aProp發生變化,OnChanges生命週期鉤子仍會被調用,這意味着一旦咱們得知輸入屬性發生變化,咱們能夠激活當前組件的變動檢測並在下一個tick中繼續detach變動檢測。

reattach(): void { 
    this._view.state |= ViewState.ChecksEnabled; 
}
複製代碼
export class ChangeDectionComponent implements OnInit, OnChanges {
    @Input() aProp: string;
    name = 'erik';
    constructor( private cd: ChangeDetectorRef, ) { }
    ngOnInit() {
    }
    ngOnChanges(change) {
        this.cd.reattach();
        setTimeout(() => {
            this.cd.detach();
        });
    }
}
複製代碼

上面這種作法幾乎與將ChangeDetectionStrategy改成OnPush是等效的。他們都在第一輪變動檢測後禁用了檢測,當父組件向子組件傳值發生變化時激活變動檢測,而後又禁用變動檢測。

須要注意的是,在這種狀況下,只有被禁用檢測分支最頂層組件的OnChanges鉤子纔會被觸發,並非這個分支的全部組件的OnChanges都會被觸發,緣由也很簡單,被禁用檢測的這個分支內不存在了變動檢測,天然內部也不會向子元素變動所傳遞的值,可是頂層的元素仍能夠接受到外部變動的輸入屬性。

譯註:其實將retach()和detach()放在ngOnChanges()和OnPush策略仍是不同的,OnPush策略的確是只有在input值的引用發生變化時纔出發變動檢測,這一點是正確的,可是OnPush策略自己並不影響組件內部的值的變化引發的變動檢測,而上例中組件內部的變動檢測也會被禁用。若是將這段邏輯放在ngDoCheck()中才更正確一點。

maskForCheck

上面的reattach()方法能夠對當前組件開啓變動檢測,然而若是這個組件的父組件或者更上層的組件的變動檢測仍被禁用,用reattach()後是沒有任何做用的。這意味着reattach()方法只對被禁用檢測分支的最頂層組件有意義。

所以咱們須要一個方法,能夠將當前元素及全部祖先元素直到根元素的變動檢測都開啓。ChangeDetectorRef提供了markForCheck方法:

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

在這個實現中,它簡單的向上迭代並啓用對全部直到根組件的祖先組件的檢查。

這個方法在何時有用呢?禁用變動檢測策略以後,ngDoCheck生命週期仍是會像ngOnChanges同樣被觸發。固然,跟OnChanges同樣,DoCheck也只會在禁用檢測分支的頂部組件上被調用。可是咱們就能夠利用這個生命週期鉤子來實現本身的業務邏輯和將這個組件標記爲能夠進行一輪變動檢測。

因爲Angular只檢測對象引用,咱們須要經過對對象的某些屬性來進行這種髒檢查:

// 這裏若是外部items變化爲改變引用位置,此組件是不會執行變動檢測的
// 可是若是在DoCheck()鉤子中調用markForCheck
// 因爲OnPush策略不影響DoCheck的執行,這樣就能夠偵測到這個變動
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

Angular提供了一個方法detectChanges,對當前組件和全部子組件運行一輪變動檢測。這個方法會無視組件的ViewState,也就是說這個方法不會改變組件的變動檢測策略,組件仍會維持原有的會被檢測或不會被檢測狀態。

export class AComponent {
  @Input() inputAProp;

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

  ngOnChanges(values) {
    this.cd.detectChanges();
  }
}
複製代碼

經過這個方法咱們能夠實現一個相似Angular.js的手動調用髒檢查。

checkNoChanges

這個方法是用來當前變動檢測沒有產生任何變化。他執行了文章第一部分1,7,8三個操做,並在發現有變動致使DOM須要更新時拋出異常。

結束!哈!

相關文章
相關標籤/搜索