深入理解 Angular 变化检测(change detection)
引言
本文分享一些講解Angular Change Detection的文章,并指出其中有意思的內容,以及自己的一些總結和引申。
Angular Change Detection Explained by thoughtram
- change detection的基本任務:用進程內的狀態(Component中的數據)來更新view(DOM)的顯示。
-
Angular Change Detection發生的時機:基本上所有的異步事件發生(并且回調函數已經執行完畢)以后,都需要觸發change detection(因為此時進程的狀態可能已經發生改變):
- 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)強意味著執行引擎難以做假設、做實時優化。
How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code 和 v8 Design Elements 講解了v8是如何優化代碼的。
- 這篇文章的后面部分講的是如何通過changeDetection: ChangeDetectionStrategy.OnPush來對變化檢測樹進行“剪枝”,進一步降低變化檢測的時間開銷。使用到Immutable Objects和ChangeDetectorRef.markForCheck。
Change Detection in Angular
作者Victor Savkin以前是Angular核心團隊的成員,現在似乎自己創建了一個Angular的企業咨詢公司。
變化檢測是有向的
“Change detectors propagate bindings from the root to leaves in the depth first order.”
“傳播”(propagate)這個詞比較生動地體現了變化檢測的特點。Angular程序員通過綁定來定義哪些數據可以傳播到view或者子組件中。數據從父組件傳播到子組件,反之不行。
并且,對組件樹的變化檢測是深度優先的。
數據傳播到view的綁定:
<span>todo: {{todo.text}}</span>數據傳播到子組件的綁定:
<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和子組件上。
OnPush
接下來就是介紹changeDetection:ChangeDetectionStrategy.OnPush了。這里我不再做過多解釋。引用作者在另一篇文章的話:
The framework will check OnPush components only when their inputs change or components’ templates emit events.
值得注意的是,作者指出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.
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個階段(按發生先后順序排序):
第一個階段結束以后才會進入第二個階段。
我們只能控制第一階段,因為回調函數是我們定義的,我們可以隨意在其中更新父組件屬性、子組件屬性、本組件屬性……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源碼。
總結
以上是生活随笔為你收集整理的深入理解 Angular 变化检测(change detection)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Netty使用Marshalling传输
- 下一篇: 多线程之线程池-各个参数的含义- 阿里,