[译] 关于Angular的变更检测(Change Detection)你需要知道这些
原文地址:Everything you need to know about change detection in Angular
如果你像我一樣,想對Angular的變更檢測機制有一個深入的理解,由于在網上并沒有多少有用的信息,你只能去看源碼。大多數文章都會提到每一個組件都會有一個屬于自己的變更檢測器(change detector),它負責檢查和這個組件,但是他們幾乎都僅限于在說怎么使用immutable 數據和變更檢測策略,這篇文章將會讓你明白為什么使用immutable可以工作,并且臟檢查機制是如何影響檢查的過程的。還有,這篇文章將會引發你對性能優化方面的一些場景的思考。
這篇文章包含2部分,第一部相當的有技術含量,它包含了一些指向源碼的鏈接,它詳細的介紹了臟檢查機制在Angular的底層是怎么運行的,所有內容是基·Angular的最新 版本-4.0.1(注:作者寫這篇文章的時候,Angular的最新版本是4.0.1), 臟檢查機制的實現在這個版本的實現和之前的2.4.1版本是不一樣的,如果你對之前版本的實現感興趣的話,你可以在這個stackoverflow的答案上學習到一些東西。
第二部分介紹了變更檢測在應用程序中該怎么使用,這部分內容既適用于之前的2.4.1版本,也使用于最新的4.0.1版本,因為這部分的API并沒有改變。
將視圖(view)作為一個核心概念
在Angular的教程中提到過,一個Angular應用程序就是一個組件樹,然而,Angular在底層用了一個低級的抽象,叫做 視圖(view)。一個視圖和一個組件之間有直接的關聯:一個視圖對應著一個組件,反之亦然。一個視圖通過一個叫component的屬性,保持著對與其所關聯的那個組件類的實例的引用。所有的操作(比如屬性檢查,DOM更新等),都會表現在視圖上面,因此從技術上來講,更正確的說法是,Angular是一個視圖樹,一個組件可以被看做是一個視圖的更高級的概念。下面是一些源碼中的關于視圖的介紹.
一個視圖是一個應用程序UI的基本組成單位,它是能夠被一起創建和銷毀的最小的一個元素集合。
在一個視圖中,元素的屬性可以改變,但是它的結構(數量和順序)不會被改變,只有通過一個ViewContainerRef來插入、移動或是刪除內嵌的視圖這些操作才可以改變元素的結構。每一個視圖可以包含多個視圖容器。
在本文中,我將交替使用組件視圖和組件的概念。
在這里有一點需要注意的是,網上的所有文章和StackOverflow上的一些回答將變更檢測視為變更檢測器對象或者`ChangeDetectorRef`,指的就是我在這里所說的視圖(view)。實際上,沒有一個單獨的對象來進行變更檢測,并且視圖才是變更檢測所運行的地方。 復制代碼每一個視圖通nodes屬性對它的子視圖有一個引用,因此,它可以在它的子視圖中執行一些操作。
視圖狀態(View state)
每一個視圖都有一個狀態,它扮演著非常重要的角色,因為根據這個狀態的值,Angular來決定是要對這個視圖以及它的子視圖進行變更檢測還是忽略掉。有許多可能的狀態,但是下面的這幾個是與本文相關的幾個。
如果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函數中的視圖進行變更檢測,它的大部分功能在子組件上執行,這個函數從主組件開始被每一個組件遞歸的調用,這就意味著隨著遞歸樹的展開,子組件在下一個調用中成為父組件。
當為特定視圖觸發此函數時,它按照指定的順序執行以下操作:
如果一個視圖是第一次被檢查,則將ViewState.firstCheck設置為true,如果是已經被檢查過了,則設置為false.
檢查并更新在子組件/指令實例上的輸入屬性。
更新子視圖變更檢測狀態(一部分是變更檢測策略的實現)。
對內嵌的視圖執行變更檢測(重復列出的這些步驟)。
如果綁定的值改變的話,在子組件中調用 OnChanges生命周期鉤子。
調用子組件的OnInit和ngDoCheck生命周期鉤子(OnInit只有在第一次檢查的時候才會被調用)。
在子視圖組件實例中更新ContentChildren queryList。
在子組件實例中調用AfterContentInit和AfterContentChecked生命周期鉤子(AfterContentInit只有在第一次檢查的時候才會被調用)。
如果當前視圖組件實例上的屬性變化的話,更新DOM插值表達式。
對子視圖執行變更檢查(重復這個列表里的步驟)。
更新當前視圖組件實例中的ViewChildren查詢列表。
在當前組件實例中調用AfterViewInit和AfterViewChecked生命周期鉤子(AfterViewInit只有在第一次檢查的時候才會被調用)。
禁用當前視圖的檢查(一部分是變更檢測策略的實現)。
基于上面的執行列表,有幾個需要強調的事情。
第一個事情就是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)。這是檢查子組件視圖狀態的地方,如果ChecksEnabled是true,那么執行變更檢測,下面是相關的代碼:
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 bindingsB: AfterContentInitB: AfterContentCheckedB: Update bindingsC: AfterContentInitC: AfterContentCheckedC: Update bindingsC: AfterViewInitC: AfterViewCheckedB: AfterViewInitB: AfterViewChecked A: AfterViewInit A: AfterViewChecked 復制代碼探索含義(Exploring the implications)
我們假設有下面的一個組件樹:
正如我們上面所學到的,每一個組件都有一個與之相關聯的組件視圖,每一個視圖初始化時的ViewState.ChecksEnabled都為true,這就意味著當Angular執行變更檢測時,組件樹上的每一個組件杜輝被檢查。
假設我們想禁用掉AComponent及它的子組件的變更檢測,我們只需要很簡單的把它的ViewState.ChecksEnabled設置為false就可以的。直接改變狀態是一個低級的操作,因此Angular為我們提供了一些在視圖上可用的公共方法。每一個組件都可以通過ChangeDetectorRef來獲得與其關聯的視圖的引用,Angular文檔中為這個類定義了如下的公共接口:
class ChangeDetectorRef {markForCheck() : voiddetach() : voidreattach() : voiddetectChanges() : voidcheckNoChanges() : 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
像本文中第一部分中所說的那樣,如果綁定的輸入屬性aProp在AppComponent中改變了,AComponent的OnChanges生命周期鉤子仍舊會觸發。這就意味著一旦我們輸入屬性改變了,我們就可以激活當前視圖的變更檢測器去執行變更檢測,然后在下個事件循環中再把它從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
變更檢測器上最后一個有用的方法是在運行當前的變更檢測時,確保沒有變化發生?;旧?#xff0c;它執行了本文第一部分那個步驟中的1,7,8的操作,并且當它發現一個綁定值變化了或是決定DOM應該要被更行的時候,將會拋出一個異常。
總結
以上是生活随笔為你收集整理的[译] 关于Angular的变更检测(Change Detection)你需要知道这些的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mysql创建数据库字符集的选择
- 下一篇: 推荐一款最新开源,基于AI人工智能UI自