深刻理解 Angular 變化檢測(change detection)

引言

本文分享一些講解Angular Change Detection的文章,並指出其中有意思的內容,以及本身的一些總結和引伸。javascript

Angular Change Detection Explained by thoughtram

  • change detection的基本任務:用進程內的狀態(Component中的數據)來更新view(DOM)的顯示。
  • Angular Change Detection發生的時機:基本上全部的異步事件發生(而且回調函數已經執行完畢)之後,都須要觸發change detection(由於此時進程的狀態可能已經發生改變):html

    • Events - click, submit, …
    • XHR - Fetching data from a remote server
    • Timers - setTimeout(), setInterval()
  • 單向數據流:沿着組件樹進行變化檢測,檢查完父組件之後再檢查子組件,在檢查父組件的時候可能會更新子組件中的綁定,可是在檢查子組件的時候(此時父組件已經檢查完畢)不會更新父組件的數據。也就是說,在變化檢測的過程當中,數據能夠從父組件流進子組件,但不會從子組件流進父組件。這是Angular與AngularJS之間的重大區別。Angular的這個特色可以保證:只須要執行一次Change Detection,就能使得view與組件中的數據一致("change detection gets stable after a single pass")。而在AngularJS中,因爲在檢查一個組件的時候可能會改動另外一個組件中的數據,所以須要屢次檢查,直到數據「穩定」下來。
    從別的文章偷來一張圖(不少文章有這張圖,已經不知道來源):
  • 專用change detector:Angular的變化檢測出了名的快,這是其中一個很重要的緣由。每一個組件都有一個本身的change detector(Angular compiler爲每一個component編譯生成專門檢測它的view的代碼),這使得每一個change detector的檢測代碼很是地簡單高效(VM friendly)。而在AngularJS中,全部component輸入同一個算法來進行變化檢測,雖然代碼的通常性(generic)、通用性很強,可是這種代碼執行的效率相對較慢,由於動態性(dynamic)強意味着執行引擎難以作假設、作實時優化。
多態(polymorphic):通用性每每意味着多態的存在。多態的含義是,一段代碼,屢次執行,可是每次執行所操做的對象都不是同一個對象,甚至這些對象的「形狀」(shape)相差巨大(屬性名不同,或者屬性被添加的順序不同)。這種代碼難以被優化。大量使用單態(monomorphic)是Angular 速度爆炸的重要緣由!除了Change Detection方面,Angular在其餘地方也使用了單態的優化思想。
How JavaScript works: inside the V8 engine + 5 tips on how to write optimized codev8 Design Elements 講解了v8是如何優化代碼的。
  • 這篇文章的後面部分講的是如何經過changeDetection: ChangeDetectionStrategy.OnPush來對變化檢測樹進行「剪枝」,進一步下降變化檢測的時間開銷。使用到Immutable ObjectsChangeDetectorRef.markForCheck

Change Detection in Angular

做者Victor Savkin之前是Angular核心團隊的成員,如今彷佛本身建立了一個Angular的企業諮詢公司。java

變化檢測是有向的

「Change detectors propagate bindings from the root to leaves in the depth first order.」
「傳播」(propagate)這個詞比較生動地體現了變化檢測的特色。Angular程序員經過綁定來定義哪些數據能夠傳播到view或者子組件中。數據從父組件傳播到子組件,反之不行。
而且,對組件樹的變化檢測是深度優先的。git

數據傳播到view的綁定:程序員

<span>todo: {{todo.text}}</span>

數據傳播到子組件的綁定:github

<todo-cmp [model]="myTodo"></todo-cmp>

變化檢測將用本組件的myTodo屬性來更新子組件的@Input() model屬性。算法

變化檢測默認檢測全部組件的緣由

"Angular has to be conservative and run all the checks every single time because the JavaScript language does not give us object mutation guarantees."
緣由在Angular Change Detection Explained by thoughtram也介紹過了。即便@Input()對象的引用沒有變,其中的屬性可能已經發生變化(JavaScript對象的動態性),變化檢測須要將這種變化也反映在view和子組件上。api

OnPush

接下來就是介紹changeDetection:ChangeDetectionStrategy.OnPush了。這裏我再也不作過多解釋。引用做者在另外一篇文章的話:
The framework will check OnPush components only when their inputs change or components’ templates emit events.session

值得注意的是,做者指出OnPush並非全部屬性都必須是immutable的,只要@Input是immutable的,而且其餘mutable的屬性能保證僅在【@Input更新】或【組件的template中有事件觸發】時才更新:
It is worth noting that a component can still have private mutable state as long as it changes only due to inputs being updated or an event being fired from within the component’s template. The only thing the OnPush strategy disallows is depending on shared mutable state. Read more about it here.app

Two Phases of Angular Applications

文章開頭歸納得很是精闢:

Angular 2 separates updating the application model(能夠理解爲更新Component的屬性值) and reflecting the state of the model in the view into two distinct phases. The developer is responsible for updating the application model. Angular, by means of change detection, is responsible for reflecting the state of the model in the view. The framework does it automatically on every VM turn.

Event bindings, which can be added using the () syntax, can be used to capture a browser event execute some function on a component. So they trigger the first phase.

Property bindings, which can be added using the [] syntax, should be used only for reflecting the state of the model in the view.

Angular應用的變化(Component屬性的變化和DOM的變化)分紅2個階段(按發生前後順序排序):

  1. 異步事件發生(好比用戶點擊或者AJAX請求完成),註冊的回調函數被執行(好比<button (click)="handler($event)"></button>),這些回調函數能夠改變Component中的屬性。
  2. 回調函數執行完畢之後(本輪事件循環的末尾),Angular此時纔會執行變化檢測。變化檢測根據咱們定義好的數據綁定([model]='prop'),將Component中的新數據「推」到view中(包括更新DOM和更新子組件的@Input屬性)。對組件樹執行變化檢測的順序:從根到葉,深度優先。

第一個階段結束之後纔會進入第二個階段。
咱們只能控制第一階段,由於回調函數是咱們定義的,咱們能夠隨意在其中更新父組件屬性、子組件屬性、本組件屬性……Angular徹底不會插手。
第二個階段由Angular來完成。這階段發生的就是變化檢測(change detection)。在變化檢測的過程當中,這些變化會在組件樹上傳播:從父組件到子組件單向傳播,以及從組件傳播到它的DOM。哪些數據傳播給子組件、更新子組件的那些屬性、更新DOM的哪些屬性……這些是由數據綁定來決定的(所以從某種意義上來講,咱們也能稍微控制第二個階段,畢竟數據綁定也是咱們來寫的)。

可見,事件綁定和數據綁定的語法雖然看起來很類似((event)=[bindProp]=),可是它們是在不一樣的階段產生做用的。

這樣劃分階段的意義

在AngularJS時代,髒檢查的執行過程當中不只會更新DOM,並且可能會更新其餘application model,但application model被更新之後,可能有別的DOM又所以須要被更新(由於DOM展現的內容依賴於application model),所以AngularJS不得不作屢次髒檢查,直到application model再也不更新。這會影響應用的性能,並且不利於Debug(由於你不知道application model是何時被誰更新的,是事件回調更新的?仍是在某次髒檢查執行過程當中被更新的?)。
再看一次這張圖:

正由於這個緣由,Angular才如此劃分階段。在Angular中,application model的更新只能有2個緣由:

  • 在第一個階段,被事件回調更新。
  • 在第二個階段,組件的@Input屬性被數據綁定更新(父組件將新數據"推"進本組件)。

開發者不須要像AngularJS時代那樣考慮髒檢查的雜亂更新過程,如今只要稍微分析一下就能知道數據是如何流動的。這讓應用的邏輯更加清晰,更容易調試和重構。

view的更新也更加簡單高效了,由於只須要執行一輪變化檢測(一輪變化檢測執行完之後數據就會穩定下來,再也不變化)。而且數據的流動方向也很是清晰,始終是從父組件流入子組件(單向數據流)。

另外,Angular開發者也不須要像AngularJS開發者那樣懼怕數據環路了(看本小節第一段的例子),由於這再也不會發生。在Angular中,在第一階段,咱們能夠更新任何父組件、子組件的數據,在第二階段也不會形成數據環路(由於在第二階段,數據的傳播是單向的)。

更多相關文章

弄懂了這幾篇文章之後,不少相關文章的內容其實大同小異。我整理了一張change detection文章列表,裏面的文章都是講得比較好的,不過只有一篇是中文。。。若是感受還不是太懂的話能夠在裏面多找幾篇閱讀。其中angularindepth的文章通常會深刻到源碼,想要更深刻理解的話能夠閱讀其中文章,乃至本身研究Angular源碼。

相關文章
相關標籤/搜索