Angular变化检测机制
2019獨角獸企業重金招聘Python工程師標準>>>
在使用Angular進行開發中,我們常用到Angular中的綁定——模型到視圖的輸入綁定、視圖到模型的輸出綁定以及視圖與模型的雙向綁定。而這些綁定的值之所以能在視圖與模型之間保持同步,正是得益于Angular中的變化檢測。
變化檢測是什么?
簡單來說變化檢測就是Angular用來檢測視圖與模型之間綁定的值是否發生了改變,當檢測到模型中綁定的值發生改變時,則同步到視圖上,反之,當檢測到視圖上綁定的值發生改變時,則回調對應的綁定函數。
什么情況下會引起變化檢測?
變化檢測的關鍵在于如何最小粒度地檢測到綁定的值是否發生了改變,那么在什么情況下會導致這些綁定的值發生變化呢?我們可以看一下我們常用的幾種場景:
@Component({ selector:?'my-app', template:?` <h1>{{name}}</h1> <button (click)="changeName()">change name</button> ` }) export class?MyApp?{ name='Tom'; changeName(){ this.name='Jerry' } }我們在視圖上通過插值表達式綁定了MyApp中的name屬性,當點擊按鈕時,改變了name屬性的值,這時就導致了綁定的值發生了變化。再來看另外一種場景:
@Component({ selector:?'my-app', template:?`<h1>{{name}}</h1>`, styleUrls: ['./app.component.css'] }) export class?MyApp?implements?OnInit{ name='Tom'; constructor(private?http:Http){} ngOnInit(){ this.http.get('/contacts') .map(res=>res.json) .subscribe(name=>this.name=name); } }我們在MyApp這個組件的生命周期鉤子函數里向服務器端發送了一個Ajax請求,當這個請求返回結果時,同樣會改變當前視圖上綁定的name屬性的值。類似的,我們還可能設定一些定時任務,這些定時任務也可能會改變與視圖綁定的值:
@Component({ selector:?'my-app', template:?`<h1>{{name}}</h1>`, styleUrls: ['./app.component.css'] }) export class?MyApp?implements?OnInit{ name='Tom'; constructor(private?http:Http){} ngOnInit(){ setTimeout(()=>{ this.name='Jerry' },1000); } }其實,我們不難發現上述三種情況都有一個共同點,即這些導致綁定值發生改變的事件都是異步發生的。如果這些異步的事件在發生時能夠通知到Angular框架,那么Angular框架就能及時的檢測到變化。
那么Angular框架如何才能獲知到這些異步事件的發生呢?我們不妨來看一看異步事件在JavaScript中執行的過程:
左邊表示將要運行的代碼,這里的stack表示JavaScript的運行棧,而webApi則是瀏覽器中提供的一些JavaScript的API,TaskQueue表示JavaScript中任務隊列,因為JavaScript是單線程的,異步任務在任務隊列中執行。
當上述代碼在JavaScript中執行時,首先func1 進入運行棧,func1執行完畢后,setTimeout進入運行棧,執行setTimeout過程中將回調函數cb 加入到任務隊列,然后setTimeout出棧,接著執行func2函數,func2函數執行完畢時,運行棧為空,接著任務隊列中cb 進入運行棧得到執行。可以看出異步任務首先會進入任務隊列,當運行棧中的同步任務都執行完畢時,異步任務進入運行棧得到執行。如果這些異步的任務執行前與執行后能提供一些鉤子函數,通過這些鉤子函數,Angular便能獲知異步任務的執行。
事實上,Angular正是使用Zonejs(它描述JavaScript執行過程的上下文,可以在異步任務之間進行持久性傳遞,它類似于Java中的TLS)來做到的。Zonejs通過猴子補丁的方式,對webApi中的一些異步任務的API在運行時進行替換,替換后的API提供了一些鉤子函數。
變化檢測是個什么樣的過程?
通過上面的介紹,我們大致明白了變化檢測是如何被觸發的,那么Angular中的變化檢測是如何執行的呢?首先我們需要知道的是,對于每一個組件,都有一個對應的變化檢測器;即每一個Component都對應有一個changeDetector,我們可以在Component中通過依賴注入來獲取到changeDetector。而我們的多個Component是一個樹狀結構的組織,由于一個Component對應一個changeDetector,那么changeDetector之間同樣是一個樹狀結構的組織。最后我們需要記住的一點是,每次變化檢測都是從樹根開始的。
枯燥無味的理論到此結束,下面通過一些例子來直觀的感受一下。
Main.component.ts : import?{Component}?from?'@angular/core'; import?{Actor}?from?'./actor.model'; @Component({ selector:?'main', template:?` <h1>MovieApp</h1> <p>{{?slogan}}</p> <button type="button" (click)="changeActorProperties()">Change Actor Properties </button> <button type="button" (click)="changeActorObject()"> ChangeActorObject </button> <movie [title]="title" [actor]="actor"></movie>` })export class?MainComponent?{ slogan:?string?=?'Just movie information'; title:?string?=?'Terminator 1'; actor:?Actor?=?new?Actor('Arnold',?'Schwarzenegger'); changeActorProperties() { this.actor.firstName?=?'Nicholas'; this.actor.lastName?=?'Cage'; }changeActorObject() { this.actor?=?new?Actor('Bruce',?'Willis'); } }Movie.component.ts: import?{Component,?Input}?from?'@angular/core'; import?{Actor}?from?'./actor.model'; @Component({ selector:?'movie', styles: ['div{border: 1px solid black}'], template:?` <div> <h3>{{?title}}</h3> <p> <label>Actor:</label> <span>{{actor.firstName}} {{actor.lastName}}</span> </p> </div>` }) export class?MovieComponent?{ @Input()?title:?string; @Input()?actor:?Actor;}上述代碼中,MainComponent通過<movie></movie> 標簽嵌入了MovieComponent,從樹狀結構上來說,MainComponent是MovieComponent的根節點,而MovieComponent是MainComponent的葉子節點。當我們點擊MainComponent的第一個button時,會回調到changeActorProperties方法,然后會觸發變化檢測的執行。首先變化檢測從MainComponent開始:
-  
檢測slogan 值是否發生了變化:沒有發生變化
 -  
檢測 title 值是否發生了變化:沒有發生變化
 -  
檢測 actor 值是否發生了變化:沒有發生變化
 
你可能對于 actor的檢測結果感到疑惑,不是明明改變了actor的屬性值嗎?實質上在對actor檢測時只檢測actor 本身的引用值是否發生了改變,改變actor的屬性值并未改變actor 本身的引用,因此是沒有發生變化。而當我們點擊MainComponent的第二個button ,重新new了一個 actor ,這時變化檢測才會檢測到 actor發生了改變。
然后變化檢測進入到葉子節點MovieComponent:
-  
檢測title 值是否發生了改變:沒有發生變化
 -  
檢測actor.firstName 是否發生了變化:發生了改變
 -  
檢測actor.lastName 是否發生了改變:發生了改變
 
因為MovieComponent再也沒有了葉子節點,所以變化檢測將更新DOM,同步視圖與模型之間的變化。
看到這里你可能會想,這機制未免也有點太簡單粗暴了吧,假如我的應用中有成百上千個Component,隨便一個Component 觸發了檢測,那么都需要從根節點到葉子節點重新檢測一遍。別著急,Angular 的開發團隊已經考慮到了這個問題,上述的檢測機制只是一種默認的檢測機制,Angular還提供一種OnPush的檢測機制,還是同樣的例子,下面看一下OnPush檢測機制是咋樣的:
Main.component.ts : import?{Component,?ChangeDetectionStrategy}?from?'@angular/core'; import?{Actor}?from?'./actor.model'; @Component({ selector:?'main', template:?` <h1>MovieApp</h1> <p>{{?slogan}}</p> <button type="button" (click)="changeActorProperties()"> Change Actor Properties </button> <button type="button" (click)="changeActorObject()"> Change Actor Object </button> <movie [title]="title" [actor]="actor"></movie>`, changeDetection:ChangeDetectionStrategy.OnPush })export class?MainComponent?{ slogan:?string?=?'Just movie information'; title:?string?=?'Terminator 1'; actor:?Actor?=?new?Actor('Arnold',?'Schwarzenegger'); changeActorProperties() { this.actor.firstName?=?'Nicholas'; this.actor.lastName?=?'Cage'; } changeActorObject() { this.actor?=?new?Actor('Bruce',?'Willis'); } }與上面的代碼相比,只在@Component中添加了:
changeDetection:ChangeDetectionStrategy.OnPush
即將檢測機制設置為OnPush。同樣的,當我們點擊第一個button時,將會發生如下變化檢測:
-  
檢測slogan 值是否發生了變化:沒有發生變化
 -  
檢測 title 值是否發生了變化:沒有發生變化
 -  
檢測 actor 值是否發生了變化:沒有發生變化
 
好,變化檢測到此結束,不會再進入到 MovieComponent 中了。這正是OnPush與Default之間的差別:當檢測到與子組件輸入綁定的值沒有發生改變時,變化檢測就不會深入到子組件中去。那么當我們點擊MainComponent中的第二個按鈕時,由于改變了actor本身而不是它的屬性值,那么就會檢測到actor的變化,進而繼續進入到MovieComponent 進行變化檢測。所以,當你使用了OnPush檢測機制時,在修改一個綁定值的屬性時,要確保同時修改到了綁定值本身的引用。但是每次需要改變屬性值的時候去new一個新的對象會使得代碼很難看,并且有時候你難以保證你一定記得這么做,恩,immutable.js 你值得擁有!
另外一個問題就是,當我們使用OnPush并且輸入綁定的是一個Observable對象時,怎么才能檢測到當訂閱的事件發生時引起的綁定的值的發生了改變呢?比如下面這個組件:
import?{Component,?Input,?ChangeDetectionStrategy}?from?'@angular/core'; import?{Observable}?from?"rxjs"; @Component({ selector:?'my-count', template:?`<span>{{count}}</span>`, changeDetection:ChangeDetectionStrategy.OnPush })export class?CountComponent?{ @Input()?addItemStream:?Observable<any>; count=0; ngOnInit(){ this.addItemStream.subscribe(()=>{ this.count++; }); } }輸入綁定 addItemStream 是一個Observable對象,Observable對象本身是不會變化的,只有當訂閱的事件到達時,才會去修改count的值。如果使用OnPush 那么檢測就不會進入到CountComponent。解決的辦法很簡單,只需在修改count的值后做一個標記(markForCheck),那么變化檢測就會沿著CountComponent所在的樹枝進行變化檢測。
import?{Component,?Input,?ChangeDetectionStrategy,?ChangeDetectorRef}?from?'@angular/core'; import?{Observable}?from?"rxjs"; @Component({ selector:?'my-count', template:?`<span>{{count}}</span>`, changeDetection:ChangeDetectionStrategy.OnPush }) export class?CountComponent?{ @Input()?addItemStream:?Observable<any>; count=0; constructor(private?cd:ChangeDetectorRef){ } ngOnInit(){ this.addItemStream.subscribe(()=>{ this.count++; this.cd.markForCheck(); }); } }總結
總結來說,Angular中變化檢測器是樹型結構的組織,與組件樹結構相對應,默認情況下,當一個組件引發了變化檢測時,檢測是從樹根開始一直檢測到樹節點。當你設置某個組件的檢測策略是 OnPush 時,如果該組件的輸入綁定沒有發生變化時,那么檢測就不會進入到該組件。當組件樹變的很龐大時,常用這種辦法來提高應用的性能。
轉載于:https://my.oschina.net/u/2285087/blog/1142820
總結
以上是生活随笔為你收集整理的Angular变化检测机制的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 用户反馈KB3189866累积更新出现卡
 - 下一篇: Linux上搭建nginx,及简单配置