溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Angular中變更檢測的示例分析

發(fā)布時間:2021-04-09 11:12:03 來源:億速云 閱讀:172 作者:小新 欄目:web開發(fā)

這篇文章主要介紹Angular中變更檢測的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

核心概念-視圖View


Angular的文檔中通篇都提到了一個Angular應(yīng)用是一個組件樹。但是Angular底層其實使用了一個低級抽象-視圖View。視圖View和組件之間的關(guān)系很直接-一個視圖與一個組件相關(guān)聯(lián),反之亦然。每個視圖都在它的component屬性中保持了一個與之關(guān)聯(lián)的組件實例的引用。所有的類似于屬性檢測、DOM更新之類的操作都是在視圖上進行的。因此,技術(shù)上而言把Angular應(yīng)用描述成一個視圖樹更加準(zhǔn)確,因為組件是視圖的一個高階描述。在源碼中有關(guān)視圖是這么描述的:

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

視圖是組成應(yīng)用界面的最小單元,它是一系列元素的組合,一起被創(chuàng)建,一起被銷毀。

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.

視圖中元素的屬性可以發(fā)生變化,但是視圖中元素的數(shù)量和順序不能變化。如果想要改變的話,需要通過VireContainerRef來執(zhí)行插入,移動和刪除操作。每個視圖都會包括多個View Container。

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

需要注意的是:網(wǎng)絡(luò)上很多文章都把我們這里所描述的視圖作為了變更檢測對象或者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;
}

視圖的狀態(tài)


每個視圖都有自己的狀態(tài),基于這些狀態(tài)的值,Angular會決定是否對這個視圖和他所有的子視圖運行變更檢測。視圖有很多狀態(tài)值,但是在這篇文章中,下面四個狀態(tài)值最為重要:

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

如果CheckedEnabled值為false或者視圖處于Errored或者Destroyed狀態(tài)時,這個視圖的變更檢測就不會執(zhí)行。默認(rèn)情況下,所有視圖初始化時都會帶上CheckEnabled,除非使用了ChangeDetectionStrategy.onPush。有關(guān)onPush我們稍后再講。這些狀態(tài)也可以被合并使用,比如一個視圖可以同時有FirstCheck和CheckEnabled兩個成員。

針對操作視圖,Angular中有一些封裝出的高級概念,詳見這里。一個概念是ViewRef。他的_view屬性囊括了組件視圖,同時它還有一個方法detectChanges。當(dāng)一個異步事件觸發(fā)時,Angular從他的最頂層的ViewRef開始觸發(fā)變更檢測,然后對子視圖繼續(xù)進行變更檢測。

ChangeDectionRef可以被注入到組件的構(gòu)造函數(shù)中。這個類的定義如下:

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
}

變更檢測操作


負(fù)責(zé)對視圖運行變更檢測的主要邏輯屬于checkAndUpdateView方法。他的大部分功能都是對子組件視圖進行操作。從宿主組件開始,這個方法被遞歸調(diào)用作用于每一個組件。這意味著當(dāng)遞歸樹展開時,在下一次調(diào)用這個方法時子組件會成為父組件。

當(dāng)在某個特定視圖上開始觸發(fā)這個方法時,以下操作會依次發(fā)生:

  • 如果這是視圖的第一次檢測,將ViewState.firstCheck設(shè)置為true,否則為false;

  • 檢查并更新子組件/指令的輸入屬性-checkAndUpdateDirectiveInline

  • 更新子視圖的變更檢測狀態(tài)(屬于變更檢測策略實現(xiàn)的一部分)

  • 對內(nèi)嵌視圖運行變更檢測(重復(fù)列表中的步驟)

  • 如果綁定的值發(fā)生變化,調(diào)用子組件的onChanges生命周期鉤子;

  • 調(diào)用子組件的OnInit和DoCheck兩個生命周期鉤子(OnInit只在第一次變更檢測時調(diào)用)

  • 在子組件視圖上更新ContentChildren列表-checkAndUpdateQuery

  • 調(diào)用子組件的AfterContentInit和AfterContentChecked(前者只在第一次檢測時調(diào)用)-callProviderLifecycles

  • 如果當(dāng)前視圖組件上的屬性發(fā)生變化,更新DOM

  • 對子視圖執(zhí)行變更檢測-callViewAction

  • 更新當(dāng)前視圖組件的ViewChildren列表-checkAndUpdateQuery

  • 調(diào)用子組件的AfterViewInit和AfterViewChecked-callProviderLifecycles

  • 對當(dāng)前視圖禁用檢測

在以上操作中有幾點需要注意

深入這些操作的含義


假設(shè)我們現(xiàn)在有一棵組件樹:

在上面的講解中我們得知了每個組件都和一個組件視圖相關(guān)聯(lián)。每個視圖都使用ViewState.checksEnabled初始化了。這意味著當(dāng)Angular開始變更檢測時,整棵組件樹上的所有組件都會被檢測;

假設(shè)此時我們需要禁用AComponent和它的子組件的變更檢測,我們只要將它的ViewState.checksEnabled設(shè)置為false就行。這聽起來很容易,但是改變state的值是一個很底層的操作,因此Angular在視圖上提供了很多方法。通過ChangeDetectorRef每個組件可以獲得與之關(guān)聯(lián)的視圖。

class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}

detach

這個方法簡單的禁止了對當(dāng)前視圖的檢測;

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

在組件中的使用方法:

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

這樣就會導(dǎo)致在接下來的變更檢測中AComponent及子組件都會被跳過。

這里有兩點需要注意:

  • 雖然我們只修改了AComponent的state值,但是他的子組件也不會被執(zhí)行變更檢測;

  • 由于AComponent及其子組件不會有變更檢測,因此他們的DOM也不會有任何更新

下面是一個簡單示例,點擊按鈕后在輸入框中修改就再也不會引起下面的p標(biāo)簽的變化,外部父組件傳遞進來的值發(fā)生變化也不會觸發(fā)變更檢測:

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發(fā)生變化,OnChanges生命周期鉤子仍會被調(diào)用,這意味著一旦我們得知輸入屬性發(fā)生變化,我們可以激活當(dāng)前組件的變更檢測并在下一個tick中繼續(xù)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是等效的。他們都在第一輪變更檢測后禁用了檢測,當(dāng)父組件向子組件傳值發(fā)生變化時激活變更檢測,然后又禁用變更檢測。

需要注意的是,在這種情況下,只有被禁用檢測分支最頂層組件的OnChanges鉤子才會被觸發(fā),并不是這個分支的所有組件的OnChanges都會被觸發(fā),原因也很簡單,被禁用檢測的這個分支內(nèi)不存在了變更檢測,自然內(nèi)部也不會向子元素變更所傳遞的值,但是頂層的元素仍可以接受到外部變更的輸入屬性。

譯注:其實將retach()和detach()放在ngOnChanges()和OnPush策略還是不一樣的,OnPush策略的確是只有在input值的引用發(fā)生變化時才出發(fā)變更檢測,這一點是正確的,但是OnPush策略本身并不影響組件內(nèi)部的值的變化引起的變更檢測,而上例中組件內(nèi)部的變更檢測也會被禁用。如果將這段邏輯放在ngDoCheck()中才更正確一點。

maskForCheck

上面的reattach()方法可以對當(dāng)前組件開啟變更檢測,然而如果這個組件的父組件或者更上層的組件的變更檢測仍被禁用,用reattach()后是沒有任何作用的。這意味著reattach()方法只對被禁用檢測分支的最頂層組件有意義。

因此我們需要一個方法,可以將當(dāng)前元素及所有祖先元素直到根元素的變更檢測都開啟。ChangeDetectorRef提供了markForCheck方法:

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

在這個實現(xiàn)中,它簡單的向上迭代并啟用對所有直到根組件的祖先組件的檢查。

這個方法在什么時候有用呢?禁用變更檢測策略之后,ngDoCheck生命周期還是會像ngOnChanges一樣被觸發(fā)。當(dāng)然,跟OnChanges一樣,DoCheck也只會在禁用檢測分支的頂部組件上被調(diào)用。但是我們就可以利用這個生命周期鉤子來實現(xiàn)自己的業(yè)務(wù)邏輯和將這個組件標(biāo)記為可以進行一輪變更檢測。

由于Angular只檢測對象引用,我們需要通過對對象的某些屬性來進行這種臟檢查:

// 這里如果外部items變化為改變引用位置,此組件是不會執(zhí)行變更檢測的
// 但是如果在DoCheck()鉤子中調(diào)用markForCheck
// 由于OnPush策略不影響DoCheck的執(zhí)行,這樣就可以偵測到這個變更
Component({
   ...,
   changeDetection: ChangeDetectionStrategy.OnPush
})
MyComponent {
    @Input() items;
    prevLength;
    constructor(cd: ChangeDetectorRef) {}

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

    ngDoCheck() {
        // 通過比較前后的數(shù)組長度
        if (this.items.length !== this.prevLength) {
            this.cd.markForCheck(); 
            this.prevLenght = this.items.length;
        }
    }
}

detectChanges

Angular提供了一個方法detectChanges,對當(dāng)前組件和所有子組件運行一輪變更檢測。這個方法會無視組件的ViewState,也就是說這個方法不會改變組件的變更檢測策略,組件仍會維持原有的會被檢測或不會被檢測狀態(tài)。

export class AComponent {
  @Input() inputAProp;

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

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

通過這個方法我們可以實現(xiàn)一個類似Angular.js的手動調(diào)用臟檢查。

checkNoChanges

這個方法是用來當(dāng)前變更檢測沒有產(chǎn)生任何變化。他執(zhí)行了文章第一部分1,7,8三個操作,并在發(fā)現(xiàn)有變更導(dǎo)致DOM需要更新時拋出異常。

以上是“Angular中變更檢測的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI