您好,登錄后才能下訂單哦!
這篇文章主要介紹Angular中變更檢測的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
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.
視圖中元素的屬性可以發生變化,但是視圖中元素的數量和順序不能變化。如果想要改變的話,需要通過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方法。他的大部分功能都是對子組件視圖進行操作。從宿主組件開始,這個方法被遞歸調用作用于每一個組件。這意味著當遞歸樹展開時,在下一次調用這個方法時子組件會成為父組件。
當在某個特定視圖上開始觸發這個方法時,以下操作會依次發生:
如果這是視圖的第一次檢測,將ViewState.firstCheck設置為true,否則為false;
檢查并更新子組件/指令的輸入屬性-checkAndUpdateDirectiveInline
更新子視圖的變更檢測狀態(屬于變更檢測策略實現的一部分)
對內嵌視圖運行變更檢測(重復列表中的步驟)
如果綁定的值發生變化,調用子組件的onChanges生命周期鉤子;
調用子組件的OnInit和DoCheck兩個生命周期鉤子(OnInit只在第一次變更檢測時調用)
在子組件視圖上更新ContentChildren列表-checkAndUpdateQuery
調用子組件的AfterContentInit和AfterContentChecked(前者只在第一次檢測時調用)-callProviderLifecycles
如果當前視圖組件上的屬性發生變化,更新DOM
對子視圖執行變更檢測-callViewAction
更新當前視圖組件的ViewChildren列表-checkAndUpdateQuery
調用子組件的AfterViewInit和AfterViewChecked-callProviderLifecycles
對當前視圖禁用檢測
在以上操作中有幾點需要注意
假設我們現在有一棵組件樹:
在上面的講解中我們得知了每個組件都和一個組件視圖相關聯。每個視圖都使用ViewState.checksEnabled初始化了。這意味著當Angular開始變更檢測時,整棵組件樹上的所有組件都會被檢測;
假設此時我們需要禁用AComponent和它的子組件的變更檢測,我們只要將它的ViewState.checksEnabled設置為false就行。這聽起來很容易,但是改變state的值是一個很底層的操作,因此Angular在視圖上提供了很多方法。通過ChangeDetectorRef
每個組件可以獲得與之關聯的視圖。
class ChangeDetectorRef { markForCheck() : void detach() : void reattach() : void detectChanges() : void checkNoChanges() : void }
這個方法簡單的禁止了對當前視圖的檢測;
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(); } }
文章第一部分提到:如果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()中才更正確一點。
上面的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; } } }
Angular提供了一個方法detectChanges
,對當前組件和所有子組件運行一輪變更檢測。這個方法會無視組件的ViewState,也就是說這個方法不會改變組件的變更檢測策略,組件仍會維持原有的會被檢測或不會被檢測狀態。
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); } }
通過這個方法我們可以實現一個類似Angular.js的手動調用臟檢查。
這個方法是用來當前變更檢測沒有產生任何變化。他執行了文章第一部分1,7,8三個操作,并在發現有變更導致DOM需要更新時拋出異常。
以上是“Angular中變更檢測的示例分析”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。