您好,登錄后才能下訂單哦!
這篇文章主要介紹Angular中變更檢測的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
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)的值,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 }
這個方法簡單的禁止了對當(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(); } }
文章第一部分提到:如果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()中才更正確一點。
上面的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; } } }
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)用臟檢查。
這個方法是用來當(dāng)前變更檢測沒有產(chǎn)生任何變化。他執(zhí)行了文章第一部分1,7,8三個操作,并在發(fā)現(xiàn)有變更導(dǎo)致DOM需要更新時拋出異常。
以上是“Angular中變更檢測的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(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)容。