- 原文地址:Everything you need to know about change detection in Angular
- 原文做者:Max, Wizard of the Web
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:tian-li
- 校對者:nanjingboy, Mcskiller
若是你想跟我同樣對 Angular 的變化檢測機制有全面的瞭解,你就不得不去查看源碼,由於網上幾乎沒有這方面的文章。大部分文章只提到每一個組件都有本身的變化檢測器,且重點在使用不可變變量(immutable)和變化檢測策略(change detection strategy)上,卻沒有進行更深刻的探討。這篇文章會帶你一塊兒瞭解爲何不可變變量能夠觸發變化檢測及變化監測策略如何 影響檢測。另外,你能夠將本文中學到的知識運用到各類須要提高性能的場景中。前端
本文包括兩部分。第一部分比較偏技術,會有不少源碼的連接。主要講解變化檢測機制是如何運做的。本文的內容是基於(當時的)最新版本 —— Angular 4.0.1。該版本中的變化檢測機制和 2.4.1 的有一點不一樣。若是你有興趣,能夠參考 Stack Overflow 上的這個回答。node
第二部分展現瞭如何應用變化檢測。因爲 2.4.1 和 4.0.1 的 API 沒有發生變化,因此這一部分對於兩個版本都適用。android
Angular 的教程上一直在說,一個 Angular 應用是一顆組件樹。然而,在 Angular 內部使用的是一種叫作視圖(view)的低階抽象。視圖和組件之間是有直接聯繫的 —— 每一個視圖都有與之關聯的組件,反之亦然。視圖經過 component
屬性將其與對應的組件類關聯起來。全部的操做都在視圖中執行,好比屬性檢查和更新 DOM。因此,從技術上來講,更正確的說法是:一個 Angular 應用是一顆視圖樹。組件能夠描述爲視圖的更高階的概念。關於視圖,源碼中有這樣一段描述:ios
視圖是構成應用 UI 的基本元素。它是一組一塊兒被創造和銷燬的最小合集。git
視圖的屬性能夠更改,而視圖中元素的結構(數量和順序)不能更改。想要改變元素的結構,只能經過用
ViewContainerRef
來插入、移動或者移除嵌入的視圖。每一個視圖能夠包含多個視圖容器(View Container)。github
在這篇文章中,我會交替使用組件視圖和組件的概念。後端
值得一提的是,網上有關變化檢測文章和 StackOverflow 中的回答中,都把本文中的視圖稱爲變化檢測器對象(Change Detector Object)或者 ChangeDetectorRef。實際上,變化檢測並無單獨的對象,它實際上是在視圖上運行的。bash
每一個視圖都經過 nodes
屬性將其與子視圖相關聯,這樣就能對子視圖進行操做。app
每一個視圖都有一個 state
屬性。這是一個很是重要的屬性,由於 Angular 會根絕這個屬性的值來肯定是否要對此視圖和全部的子視圖執行變化檢測。state
屬性有不少可能的值,與本文相關的有如下幾種:dom
若是 CheckesEnabled
是 false
或者視圖的狀態是 Errored
或者 Destroyed
,變化檢測就會跳過此視圖和其全部子視圖。默認狀況下,全部的視圖都以 ChecksEnabled
做爲初始值,除非使用了 ChangeDetectionStrategy.OnPush
。後面會對此進行更多的解釋。視圖的能夠同時有多個狀態,好比,能夠同時是 FirstCheck
和 ChecksEnabled
。
Angular 中有不少高階概念來操做視圖。我在這篇文章中講過其中一些。其中一個概念是 ViewRef。它封裝了底層組件視圖,裏面還有一個命名很恰當的方法,叫作 detectChanges
。當異步事件發生時,Angular 會在最頂層的 ViewRef 上觸發變化檢測。最頂層的 ViewRef 本身執行了變化檢測後,就會對其子視圖進行變化檢測。
你可使用 ChangeDetectorRef
令牌來將 viewRef
注入到組件的構造函數中:
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
查詢列表AfterContentInit
和 AfterContentChecked
生命週期鉤子(AfterContentInit
只會在第一次檢測時調用)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
的文檔中說,只有在它的綁定發生變化時,纔會執行檢測。因此要設置 CheckesEnabled
位來啓用檢測。下面這段代碼就是這個做用(第 2 步操做):
if (compView.def.flags & ViewFlags.OnPush) {
compView.state |= ViewState.ChecksEnabled;
}
複製代碼
只有當父視圖的綁定發生了變化,且子組件視圖初始化爲 ChangeDetectionStrategy.OnPush
時,纔會更新狀態。
最後,當前視圖的變化檢測也負責啓動子視圖的變化檢測(第 8 步)。此處會檢查子組件視圖的狀態,若是是 ChecksEnabled
,那麼就對其執行變化檢測。這是相關的代碼:
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
,也就是說當 Angular 進行變化檢測時,這棵樹中的每個組件都會被檢測。
假如咱們想禁用 AComponent
和它的子組件的變化檢測,只須要將 ViewState.ChecksEnabled
設置爲 false
。因爲改變狀態是低階操做,因此 Angular 爲咱們提供了許多視圖的公共方法。每一個組件均可以經過 ChangeDetectorRef
令牌來獲取與之相關聯的視圖。Angular 文檔中對這個類定義了以下公共接口:
class ChangeDetectorRef {
markForCheck() : void
detach() : void
reattach() : void
detectChanges() : void
checkNoChanges() : void
}
複製代碼
來看下咱們能夠如何使用這些接口。
第一個容許咱們操做狀態的是 detach
,它能夠對當前視圖禁用檢查:
detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }
複製代碼
來看下如何在代碼中使用:
export class AComponent {
constructor(public cd: ChangeDetectorRef) {
this.cd.detach();
}
複製代碼
這保證了在接下來的變化檢測中,從 AComponent
開始,左子樹都會被跳過(橙色的組件都不會被檢測):
這裏須要注意兩點——首先,儘管咱們改變的是 AComponent
的狀態,其全部子組件都不會被檢測。第二,因爲整個左子樹的組件都不執行變化檢測,它們模板中的 DOM 也不會更新。下面的例子簡單描述了一下這種狀況:
@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
。兩秒以後,changed
屬性變成了 true
,span
中的文字並不會更新。然而,若是去掉 this.cd.detach()
,就會按照預想的樣子更新了。
如第一部分所說,若是 AComponent
的輸入綁定 aProp
發生了變化,AComponent
的 Onchanges
聲明週期鉤子就會被觸發。這意味着一旦咱們得知輸入屬性發生了變化,就能夠對當前組件啓動變化檢測器來檢測變化,而後在下一個週期將其分離。這段代碼就是這個做用:
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 元素仍會隨着輸入綁定的變化而變化。
這是變化檢測器的最後一個方法,其主要做用是保證當前執行的變化檢測中,不會有變化發生。簡單來講,它執行本文第一部分提到的列表中的第 一、七、8 步。若是發現綁定發生了變化或者 DOM 須要更新,就拋出異常。
對於本文若是你有任何問題,請到 Stack Overflow 提問,而後在本文評論區貼上連接。這樣整個社區都能受益。謝謝。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。