深入理解 Angular 变化检测(change detection)
引言
本文分享一些講解Angular Change Detection的文章,并指出其中有意思的內(nèi)容,以及自己的一些總結(jié)和引申。
Angular Change Detection Explained by thoughtram
- change detection的基本任務(wù):用進(jìn)程內(nèi)的狀態(tài)(Component中的數(shù)據(jù))來更新view(DOM)的顯示。
-
Angular Change Detection發(fā)生的時機(jī):基本上所有的異步事件發(fā)生(并且回調(diào)函數(shù)已經(jīng)執(zhí)行完畢)以后,都需要觸發(fā)change detection(因?yàn)榇藭r進(jìn)程的狀態(tài)可能已經(jīng)發(fā)生改變):
- Events - click, submit, …
- XHR - Fetching data from a remote server
- Timers - setTimeout(), setInterval()
- 單向數(shù)據(jù)流:沿著組件樹進(jìn)行變化檢測,檢查完父組件以后再檢查子組件,在檢查父組件的時候可能會更新子組件中的綁定,但是在檢查子組件的時候(此時父組件已經(jīng)檢查完畢)不會更新父組件的數(shù)據(jù)。也就是說,在變化檢測的過程中,數(shù)據(jù)可以從父組件流進(jìn)子組件,但不會從子組件流進(jìn)父組件。這是Angular與AngularJS之間的重大區(qū)別。Angular的這個特點(diǎn)能夠保證:只需要執(zhí)行一次Change Detection,就能使得view與組件中的數(shù)據(jù)一致("change detection gets stable after a single pass")。而在AngularJS中,由于在檢查一個組件的時候可能會改動另一個組件中的數(shù)據(jù),因此需要多次檢查,直到數(shù)據(jù)“穩(wěn)定”下來。
從別的文章偷來一張圖(很多文章有這張圖,已經(jīng)不知道來源):
- 專用change detector:Angular的變化檢測出了名的快,這是其中一個很重要的原因。每個組件都有一個自己的change detector(Angular compiler為每個component編譯生成專門檢測它的view的代碼),這使得每個change detector的檢測代碼非常地簡單高效(VM friendly)。而在AngularJS中,所有component輸入同一個算法來進(jìn)行變化檢測,雖然代碼的一般性(generic)、通用性很強(qiáng),但是這種代碼執(zhí)行的效率相對較慢,因?yàn)閯討B(tài)性(dynamic)強(qiáng)意味著執(zhí)行引擎難以做假設(shè)、做實(shí)時優(yōu)化。
How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code 和 v8 Design Elements 講解了v8是如何優(yōu)化代碼的。
- 這篇文章的后面部分講的是如何通過changeDetection: ChangeDetectionStrategy.OnPush來對變化檢測樹進(jìn)行“剪枝”,進(jìn)一步降低變化檢測的時間開銷。使用到Immutable Objects和ChangeDetectorRef.markForCheck。
Change Detection in Angular
作者Victor Savkin以前是Angular核心團(tuán)隊(duì)的成員,現(xiàn)在似乎自己創(chuàng)建了一個Angular的企業(yè)咨詢公司。
變化檢測是有向的
“Change detectors propagate bindings from the root to leaves in the depth first order.”
“傳播”(propagate)這個詞比較生動地體現(xiàn)了變化檢測的特點(diǎn)。Angular程序員通過綁定來定義哪些數(shù)據(jù)可以傳播到view或者子組件中。數(shù)據(jù)從父組件傳播到子組件,反之不行。
并且,對組件樹的變化檢測是深度優(yōu)先的。
數(shù)據(jù)傳播到view的綁定:
<span>todo: {{todo.text}}</span>數(shù)據(jù)傳播到子組件的綁定:
<todo-cmp [model]="myTodo"></todo-cmp>變化檢測將用本組件的myTodo屬性來更新子組件的@Input() model屬性。
變化檢測默認(rèn)檢測所有組件的原因
"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()對象的引用沒有變,其中的屬性可能已經(jīng)發(fā)生變化(JavaScript對象的動態(tài)性),變化檢測需要將這種變化也反映在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中有事件觸發(fā)】時才更新:
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應(yīng)用的變化(Component屬性的變化和DOM的變化)分成2個階段(按發(fā)生先后順序排序):
第一個階段結(jié)束以后才會進(jìn)入第二個階段。
我們只能控制第一階段,因?yàn)榛卣{(diào)函數(shù)是我們定義的,我們可以隨意在其中更新父組件屬性、子組件屬性、本組件屬性……Angular完全不會插手。
第二個階段由Angular來完成。這階段發(fā)生的就是變化檢測(change detection)。在變化檢測的過程中,這些變化會在組件樹上傳播:從父組件到子組件單向傳播,以及從組件傳播到它的DOM。哪些數(shù)據(jù)傳播給子組件、更新子組件的那些屬性、更新DOM的哪些屬性……這些是由數(shù)據(jù)綁定來決定的(因此從某種意義上來說,我們也能稍微控制第二個階段,畢竟數(shù)據(jù)綁定也是我們來寫的)。
可見,事件綁定和數(shù)據(jù)綁定的語法雖然看起來很相似((event)=和[bindProp]=),但是它們是在不同的階段產(chǎn)生作用的。
這樣劃分階段的意義
在AngularJS時代,臟檢查的執(zhí)行過程中不僅會更新DOM,而且可能會更新其他application model,但application model被更新以后,可能有別的DOM又因此需要被更新(因?yàn)镈OM展示的內(nèi)容依賴于application model),因此AngularJS不得不做多次臟檢查,直到application model不再更新。這會影響應(yīng)用的性能,而且不利于Debug(因?yàn)槟悴恢繿pplication model是什么時候被誰更新的,是事件回調(diào)更新的?還是在某次臟檢查執(zhí)行過程中被更新的?)。
再看一次這張圖:
正因?yàn)檫@個原因,Angular才如此劃分階段。在Angular中,application model的更新只能有2個原因:
- 在第一個階段,被事件回調(diào)更新。
- 在第二個階段,組件的@Input屬性被數(shù)據(jù)綁定更新(父組件將新數(shù)據(jù)"推"進(jìn)本組件)。
開發(fā)者不需要像AngularJS時代那樣考慮臟檢查的雜亂更新過程,現(xiàn)在只要稍微分析一下就能知道數(shù)據(jù)是如何流動的。這讓應(yīng)用的邏輯更加清晰,更容易調(diào)試和重構(gòu)。
view的更新也更加簡單高效了,因?yàn)橹恍枰獔?zhí)行一輪變化檢測(一輪變化檢測執(zhí)行完以后數(shù)據(jù)就會穩(wěn)定下來,不再變化)。并且數(shù)據(jù)的流動方向也非常清晰,始終是從父組件流入子組件(單向數(shù)據(jù)流)。
另外,Angular開發(fā)者也不需要像AngularJS開發(fā)者那樣害怕數(shù)據(jù)環(huán)路了(看本小節(jié)第一段的例子),因?yàn)檫@不再會發(fā)生。在Angular中,在第一階段,我們可以更新任何父組件、子組件的數(shù)據(jù),在第二階段也不會造成數(shù)據(jù)環(huán)路(因?yàn)樵诘诙A段,數(shù)據(jù)的傳播是單向的)。
更多相關(guān)文章
弄懂了這幾篇文章以后,很多相關(guān)文章的內(nèi)容其實(shí)大同小異。我整理了一張change detection文章列表,里面的文章都是講得比較好的,不過只有一篇是中文。。。如果感覺還不是太懂的話可以在里面多找?guī)灼喿x。
其中angularindepth的文章一般會深入到源碼,想要更深入理解的話可以閱讀其中文章,乃至自己研究Angular源碼。
總結(jié)
以上是生活随笔為你收集整理的深入理解 Angular 变化检测(change detection)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Netty使用Marshalling传输
- 下一篇: 多线程之线程池-各个参数的含义- 阿里,