原文地址:Everything you need to know about change detection in Angularnode
若是你像我同樣,想對Angular
的變動檢測機制有一個深刻的理解,因爲在網上並無多少有用的信息,你只能去看源碼。大多數文章都會提到每個組件都會有一個屬於本身的變動檢測器(change detector
),它負責檢查和這個組件,可是他們幾乎都僅限於在說怎麼使用immutable
數據和變動檢測策略,這篇文章將會讓你明白爲何使用immutable
能夠工做,而且髒檢查機制是如何影響檢查的過程的。還有,這篇文章將會引起你對性能優化方面的一些場景的思考。git
這篇文章包含2部分,第一部至關的有技術含量,它包含了一些指向源碼的連接,它詳細的介紹了髒檢查機制在Angular
的底層是怎麼運行的,全部內容是基·Angular的
最新 版本-4.0.1
(注:做者寫這篇文章的時候,Angular
的最新版本是4.0.1
), 髒檢查機制的實如今這個版本的實現和以前的2.4.1
版本是不同的,若是你對以前版本的實現感興趣的話,你能夠在這個stackoverflow的答案上學習到一些東西。github
第二部分介紹了變動檢測在應用程序中該怎麼使用,這部份內容既適用於以前的2.4.1
版本,也使用於最新的4.0.1
版本,由於這部分的API並無改變。性能優化
在Angular的教程中提到過,一個Angular應用程序就是一個組件樹,然而,Angular在底層用了一個低級的抽象,叫作 視圖(view)。一個視圖和一個組件之間有直接的關聯:一個視圖對應着一個組件,反之亦然。一個視圖經過一個叫component
的屬性,保持着對與其所關聯的那個組件類的實例的引用。全部的操做(好比屬性檢查,DOM更新等),都會表如今視圖上面,所以從技術上來說,更正確的說法是,Angular
是一個視圖樹,一個組件能夠被看作是一個視圖的更高級的概念。下面是一些源碼中的關於視圖的介紹.bash
一個視圖是一個應用程序UI的基本組成單位,它是可以被一塊兒建立和銷燬的最小的一個元素集合。
在一個視圖中,元素的屬性能夠改變,可是它的結構(數量和順序)不會被改變,只有經過一個ViewContainerRef
來插入、移動或是刪除內嵌的視圖這些操做才能夠改變元素的結構。每個視圖能夠包含多個視圖容器。markdown
在本文中,我將交替使用組件視圖和組件的概念。dom
在這裏有一點須要注意的是,網上的全部文章和StackOverflow上的一些回答將變動檢測視爲變動檢測器對象或者`ChangeDetectorRef`,指的就是我在這裏所說的視圖(view)。實際上,沒有一個單獨的對象來進行變動檢測,而且視圖纔是變動檢測所運行的地方。
複製代碼
每個視圖通nodes屬性對它的子視圖有一個引用,所以,它能夠在它的子視圖中執行一些操做。異步
每個視圖都有一個狀態,它扮演着很是重要的角色,由於根據這個狀態的值,Angular來決定是要對這個視圖以及它的子視圖進行變動檢測仍是忽略掉。有許多可能的狀態,可是下面的這幾個是與本文相關的幾個。ide
若是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 bindings
B: AfterContentInit
B: AfterContentChecked
B: Update bindings
C: AfterContentInit
C: AfterContentChecked
C: Update bindings
C: AfterViewInit
C: AfterViewChecked
B: AfterViewInit
B: AfterViewChecked
A: AfterViewInit
A: AfterViewChecked
複製代碼
咱們假設有下面的一個組件樹:
正如咱們上面所學到的,每個組件都有一個與之相關聯的組件視圖,每個視圖初始化時的ViewState.ChecksEnabled
都爲true
,這就意味着當Angular執行變動檢測時,組件樹上的每個組件杜輝被檢查。
假設咱們想禁用掉AComponent
及它的子組件的變動檢測,咱們只須要很簡單的把它的ViewState.ChecksEnabled
設置爲false
就能夠的。直接改變狀態是一個低級的操做,所以Angular爲咱們提供了一些在視圖上可用的公共方法。每個組件均可以經過ChangeDetectorRef
來得到與其關聯的視圖的引用,Angular文檔中爲這個類定義了以下的公共接口:
class ChangeDetectorRef {
markForCheck() : void
detach() : void
reattach() : void
detectChanges() : void
checkNoChanges() : void
}
複製代碼
讓咱們看看咱們看以從中收穫點什麼吧。
第一個咱們能夠操做視圖的方法是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()
的時候,一切都會如期執行。
像本文中第一部分中所說的那樣,若是綁定的輸入屬性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
鉤子纔會被觸發,而不是禁用分支的全部組件。
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方法, 這個方法在運行變動檢測時候無論當前組件的狀態是什麼,那就意味着當前的視圖可能會保持禁用檢查的狀態,在下一個常規的變動檢測進行時,它將不會被檢查,下面是一個例子:
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); } 複製代碼
當輸入屬性改變的時候,即便變動檢測器還保持着分離的狀態,DOM也會更新。
變動檢測器上最後一個有用的方法是在運行當前的變動檢測時,確保沒有變化發生。基本上,它執行了本文第一部分那個步驟中的1,7,8的操做,而且當它發現一個綁定值變化了或是決定DOM應該要被更行的時候,將會拋出一個異常。