溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

詳解Angular系列之變化檢測(Change Detection)

發(fā)布時間:2020-08-21 20:44:42 來源:腳本之家 閱讀:271 作者:escaple_plan 欄目:web開發(fā)

概述

簡單來說變化檢測就是Angular用來檢測視圖與模型之間綁定的值是否發(fā)生了改變,當檢測到模型中綁定的值發(fā)生改變時,則同步到視圖上,反之,當檢測到視圖上綁定的值發(fā)生改變時,則回調(diào)對應的綁定函數(shù)。

什么情況下會引起變化檢測?

總結起來, 主要有如下幾種情況可能也改變數(shù)據(jù):

  1. 用戶輸入操作,比如點擊,提交等
  2. 請求服務端數(shù)據(jù)(XHR)
  3. 定時事件,比如setTimeout,setInterval

上述三種情況都有一個共同點,即這些導致綁定值發(fā)生改變的事件都是異步發(fā)生的。如果這些異步的事件在發(fā)生時能夠通知到Angular框架,那么Angular框架就能及時的檢測到變化。

詳解Angular系列之變化檢測(Change Detection)

左邊表示將要運行的代碼,這里的stack表示Javascript的運行棧,而webApi則是瀏覽器中提供的一些Javascript的API,TaskQueue表示Javascript中任務隊列,因為Javascript是單線程的,異步任務在任務隊列中執(zhí)行。

具體來說,異步執(zhí)行的運行機制如下:

  1. 所有同步任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
  2. 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之 中放置一個事件。
  3. 一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢,系統(tǒng)就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。
  4. 主線程不斷重復上面的第三步。

當上述代碼在Javascript中執(zhí)行時,首先func1 進入運行棧,func1執(zhí)行完畢后,setTimeout進入運行棧,執(zhí)行setTimeout過程中將回調(diào)函數(shù)cb 加入到任務隊列,然后setTimeout出棧,接著執(zhí)行func2函數(shù),func2函數(shù)執(zhí)行完畢時,運行棧為空,接著任務隊列中cb 進入運行棧得到執(zhí)行??梢钥闯霎惒饺蝿帐紫葧M入任務隊列,當運行棧中的同步任務都執(zhí)行完畢時,異步任務進入運行棧得到執(zhí)行。如果這些異步的任務執(zhí)行前與執(zhí)行后能提供一些鉤子函數(shù),通過這些鉤子函數(shù),Angular便能獲知異步任務的執(zhí)行。

angular2 獲取變化通知

那么問題來了,angular2是如何知道數(shù)據(jù)發(fā)生了改變?又是如何知道需要修改DOM的位置,準確的最小范圍的修改DOM呢?沒錯,盡可能小的范圍修改DOM,因為操作DOM對于性能來說可是一件奢侈品。

在AngularJS中是由代碼$scope.$apply()或者$scope.$digest觸發(fā),而Angular接入了ZoneJS,由它監(jiān)聽了Angular所有的異步事件。

ZoneJS是怎么做到的呢?

實際上Zone有一個叫猴子補丁的東西。在Zone.js運行時,就會為這些異步事件做一層代理包裹,也就是說Zone.js運行后,調(diào)用setTimeout、addEventListener等瀏覽器異步事件時,不再是調(diào)用原生的方法,而是被猴子補丁包裝過后的代理方法。代理里setup了鉤子函數(shù), 通過這些鉤子函數(shù), 可以方便的進入異步任務執(zhí)行的上下文.

//以下是Zone.js啟動時執(zhí)行邏輯的抽象代碼片段
function zoneAwareAddEventListener() {...}
function zoneAwareRemoveEventListener() {...}
function zoneAwarePromise() {...}
function patchTimeout() {...}
window.prototype.addEventListener=zoneAwareAddEventListener;
window.prototype.removeEventListener=zoneAwareRemoveEventListener;
window.prototype.promise = zoneAwarePromise;
window.prototype.setTimeout = patchTimeout;

變化檢測的過程

Angular的核心是組件化,組件的嵌套會使得最終形成一棵組件樹。Angular的變化檢測可以分組件進行,每一個Component都對應有一個changeDetector,我們可以在Component中通過依賴注入來獲取到changeDetector。而我們的多個Component是一個樹狀結構的組織,由于一個Component對應一個changeDetector,那么changeDetector之間同樣是一個樹狀結構的組織.

另外,Angular的數(shù)據(jù)流是自頂而下,從父組件到子組件單向流動。單向數(shù)據(jù)流向保證了高效、可預測的變化檢測。盡管檢查了父組件之后,子組件可能會改變父組件的數(shù)據(jù)使得父組件需要再次被檢查,這是不被推薦的數(shù)據(jù)處理方式。在開發(fā)模式下,Angular會進行二次檢查,如果出現(xiàn)上述情況,二次檢查就會報錯:Expression Changed After It Has Been Checked Error。而在生產(chǎn)環(huán)境中,臟檢查只會執(zhí)行一次。

相比之下,AngularJS采用的是雙向數(shù)據(jù)流,錯綜復雜的數(shù)據(jù)流使得它不得不多次檢查,使得數(shù)據(jù)最終趨向穩(wěn)定。理論上,數(shù)據(jù)可能永遠不穩(wěn)定。AngularJS給出的策略是,臟檢查超過10次,就認為程序有問題,不再進行檢查。

詳解Angular系列之變化檢測(Change Detection)

變化檢測策略

Angular有兩種變化檢測策略。Default是Angular默認的變化檢測策略,也就是上述提到的臟檢查,只要有值發(fā)生變化,就全部從父組件到所有子組件進行檢查,。另一種更加高效的變化檢測方式:OnPush。OnPush策略,就是只有當輸入數(shù)據(jù)(即@Input)的引用發(fā)生變化或者有事件觸發(fā)時,組件才進行變化檢測。

defalut 策略

main.component.ts

@Component({
 selector: 'app-root',
 template: `
 <h2>變更檢測策略</h2>
 <p>{{ slogan }}</p>
 <button type="button" (click)="changeStar()"> 改變明星屬性
 </button>
 <button type="button" (click)="changeStarObject()">
   改變明星對象
 </button>
 <movie [title]="title" [star]="star"></movie>`,
})
export class AppComponent {
 slogan: string = 'change detection';
 title: string = 'default 策略';
 star: Star = new Star('周', '杰倫');
 changeStar() {
  this.star.firstName = '吳';
  this.star.lastName = '彥祖';
 }
 changeStarObject() {
  this.star = new Star('劉', '德華');
 } 
}

movie.component.ts

@Component({
 selector: 'movie',
 styles: ['div {border: 1px solid black}'],
 template: `
<div>
<h4>{{ title }}</h4>
<p>
<label>Star:</label>
<span>{{star.firstName}} {{star.lastName}}</span>
</p>
</div>`,

})
export class MovieComponent {
 @Input() title: string;
 @Input() star;
}

上面代碼中, 當點擊第一個按鈕改變明星屬性時,依次對slogan, title, star三個屬性進行檢測, 此時三個屬性都沒有變化, star沒有發(fā)生變化,是因為實質(zhì)上在對star檢測時只檢測star本身的引用值是否發(fā)生了改變,改變star的屬性值并未改變star本身的引用,因此是沒有發(fā)生變化。

而當我們點擊第二個按鈕改變明星對象時 ,重新new了一個 star ,這時變化檢測才會檢測到 star發(fā)生了改變。

然后變化檢測進入到子組件中,檢測到star.firstName和star.lastName發(fā)生了變化, 然后更新視圖.

OnPush策略

與上面代碼相比, 只在movie.component.ts中的@component中增加了一行代碼:

changeDetection:ChangeDetectionStrategy.OnPush
此時, 當點擊第一個按鈕時, 檢測到star沒有發(fā)生變化, ok,變化檢測到此結束, 不會進入到子組件中, 視圖不會發(fā)生變化.

當點擊第二個按鈕時,檢測到star發(fā)生了變化, 然后變化檢測進入到子組件中,檢測到star.firstName和star.lastName發(fā)生了變化, 然后更新視圖.

所以,當你使用了OnPush檢測機制時,在修改一個綁定值的屬性時,要確保同時修改到了綁定值本身的引用。但是每次需要改變屬性值的時候去new一個新的對象會很麻煩,immutable.js 你值得擁有!

變化檢測對象引用

通過引用變化檢測對象ChangeDetectorRef,可以手動去操作變化檢測。我們可以在組件中的通過依賴注入的方式來獲取該對象:

constructor(
  private changeRef:ChangeDetectorRef
 ){}

變化檢測對象提供的方法有以下幾種:

  1. markForCheck() - 在組件的 metadata 中如果設置了 changeDetection:ChangeDetectionStrategy.OnPush 條件,那么變化檢測不會再次執(zhí)行,除非手動調(diào)用該方法, 該方法的意思是在變化監(jiān)測時必須檢測該組件。
  2. detach() - 從變化檢測樹中分離變化檢測器,該組件的變化檢測器將不再執(zhí)行變化檢測,除非手動調(diào)用 reattach() 方法。
  3. reattach() - 重新添加已分離的變化檢測器,使得該組件及其子組件都能執(zhí)行變化檢測
  4. detectChanges() - 從該組件到各個子組件執(zhí)行一次變化檢測

OnPush策略下手動發(fā)起變化檢測

組件中添加事件改變輸入屬性

在上面代碼movie.component.ts中修改如下

@Component({
 selector: 'movie',
 styles: ['div {border: 1px solid black}'],
 template: `
<div>
<h4>{{ title }}</h4>
<p>
<button (click)="changeStar()">點擊切換名字</button>    
<label>Star:</label>
<span>{{star.firstName}} {{star.lastName}}</span>
</p>
</div>`,
changeDetection:ChangeDetectionStrategy.OnPush
})
export class MovieComponent {
 constructor(
  private changeRef:ChangeDetectorRef
 ){}
 @Input() title: string;
 @Input() star;
 
 changeStar(){
  this.star.lastName = 'xjl';
 }
}

此時點擊按鈕切換名字時,star更改如下

![圖片描述][3]

第二種就是上面講到的使用變化檢測對象中的 markForCheck()方法.

ngOnInit() {
  setInterval(() => {
   this.star.lastName = 'xjl';
   this.changeRef.markForCheck();
  }, 1000);
 }

輸入屬性為Observable

修改app.component.ts

@Component({
 selector: 'app-root',
 template: `
 <h2>變更檢測策略</h2>
 <p>{{ slogan }}</p>
 <button type="button" (click)="changeStar()"> 改變明星屬性
 </button>
 <button type="button" (click)="changeStarObject()">
   改變明星對象
 </button>
 <movie [title]="title" [star]="star" [addCount]="count"></movie>`,
})
export class AppComponent implements OnInit{
 slogan: string = 'change detection';
 title: string = 'OnPush 策略';
 star: Star = new Star('周', '杰倫');
 count:Observable<any>;

 ngOnInit(){
  this.count = Observable.timer(0, 1000)
 }
 changeStar() {
  this.star.firstName = '吳';
  this.star.lastName = '彥祖';
 }
 changeStarObject() {
  this.star = new Star('劉', '德華');
 } 
}

此時,有兩種方式讓MovieComponent進入檢測,一種是使用變化檢測對象中的 markForCheck()方法.

ngOnInit() {
  this.addCount.subscribe(() => {
   this.count++;
   this.changeRef.markForCheck();
  })

另外一種是使用async pipe 管道

@Component({
 selector: 'movie',
 styles: ['div {border: 1px solid black}'],
 template: `
<div>
<h4>{{ title }}</h4>
<p>
<button (click)="changeStar()">點擊切換名字</button>    
<label>Star:</label>
<span>{{star.firstName}} {{star.lastName}}</span>
</p>
<p>{{addCount | async}}</p>
</div>`,
 changeDetection: ChangeDetectionStrategy.OnPush
})

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI