首頁 > web前端 > js教程 > 詳細解讀Angular系列中變化檢測問題

詳細解讀Angular系列中變化檢測問題

亚连
發布: 2018-06-04 14:39:39
原創
1482 人瀏覽過

這篇文章主要介紹了詳解Angular系列之變化檢測(Change Detection),現在分享給大家,也給大家做個參考。

概述

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

什麼情況會造成變化偵測?

總結起來, 主要有以下幾種情況可能也改變資料:

  1. 使用者輸入操作,例如點擊,提交等

  2. 請求服務端資料(XHR)

  3. #定時事件,例如setTimeout,setInterval

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

左邊表示將要執行的程式碼,這裡的stack表示Javascript的運行棧,而webApi則是瀏覽器中提供的一些Javascript的API,TaskQueue表示Javascript中任務佇列,因為Javascript是單執行緒的,非同步任務在任務佇列中執行。

具體來說,非同步執行的運行機制如下:

  1. 所有同步任務都在主執行緒上執行,形成一個執行堆疊(execution context stack)。

  2. 主執行緒之外,還存在一個"任務隊列"(task queue)。只要非同步任務有了運行結果,就在"任務隊列"之 中放置一個事件。

  3. 一旦"執行堆疊"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

  4. 主執行緒不斷重複上面的第三步。

當上述程式碼在Javascript中執行時,首先func1 進入運行棧,func1執行完畢後,setTimeout進入運行棧,執行setTimeout過程中將回呼函數cb 加入任務佇列,接著setTimeout出棧,接著執行func2函數,func2函數執行完畢時,運行棧為空,接著任務佇列中cb 進入運行棧得到執行。可以看出非同步任務首先會進入任務佇列,當執行堆疊中的同步任務都執行完畢時,非同步任務進入運行堆疊會執行。如果這些非同步的任務執行前與執行後能提供一些鉤子函數,透過這些鉤子函數,Angular便能獲知非同步任務的執行。

angular2 取得變更通知

那麼問題來了,angular2是如何知道資料改變了?又是如何知道需要修改DOM的位置,準確的最小範圍的修改DOM呢?沒錯,盡可能小的範圍修改DOM,因為操作DOM對於性能來說可是一件奢侈品。

在AngularJS中是由程式碼$scope.$apply()或$scope.$digest觸發,而Angular接入了ZoneJS,由它監聽了Angular所有的非同步事件。

ZoneJS是怎麼做到的呢?

其實Zone有一個叫猴子補丁的東西。在Zone.js運行時,就會為這些非同步事件做一層代理程式包裹,也就是說Zone.js運行後,呼叫setTimeout、addEventListener等瀏覽器非同步事件時,不再是呼叫原生的方法,而是被猴子補丁包裝過後的代理方法。代理程式裡setup了鉤子函數, 透過這些鉤子函數, 可以方便的進入非同步任務執行的上下文.

//以下是Zone.js启动时执行逻辑的抽象代码片段
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的資料流是自頂而下,從父元件到子元件單向流動。單向資料流向保證了高效、可預測的變化檢測。儘管檢查了父元件之後,子元件可能會改變父元件的資料使得父元件需要再次被檢查,這是不被推薦的資料處理方式。在開發模式下,Angular會進行二次檢查,如果出現上述情況,二次檢查就會報錯:Expression Changed After It Has Been Checked Error。而在生產環境中,髒檢查只會執行一次。

相比之下,AngularJS采用的是双向数据流,错综复杂的数据流使得它不得不多次检查,使得数据最终趋向稳定。理论上,数据可能永远不稳定。AngularJS给出的策略是,脏检查超过10次,就认为程序有问题,不再进行检查。

变化检测策略

Angular有两种变化检测策略。Default是Angular默认的变化检测策略,也就是上述提到的脏检查,只要有值发生变化,就全部从父组件到所有子组件进行检查,。另一种更加高效的变化检测方式:OnPush。OnPush策略,就是只有当输入数据(即@Input)的引用发生变化或者有事件触发时,组件才进行变化检测。

defalut 策略

main.component.ts

@Component({
 selector: 'app-root',
 template: `
 <h1>变更检测策略</h1>
 <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 = &#39;change detection&#39;;
 title: string = &#39;default 策略&#39;;
 star: Star = new Star(&#39;周&#39;, &#39;杰伦&#39;);
 changeStar() {
  this.star.firstName = &#39;吴&#39;;
  this.star.lastName = &#39;彦祖&#39;;
 }
 changeStarObject() {
  this.star = new Star(&#39;刘&#39;, &#39;德华&#39;);
 } 
}
登入後複製

movie.component.ts

@Component({
 selector: &#39;movie&#39;,
 styles: [&#39;p {border: 1px solid black}&#39;],
 template: `
<p>
<h3>{{ title }}</h3>
<p>
<label>Star:</label>
<span>{{star.firstName}} {{star.lastName}}</span>
</p>
</p>`,

})
export class MovieComponent {
 @Input() title: string;
 @Input() star;
}
登入後複製

上面代码中, 当点击第一个按钮改变明星属性时,依次对slogan, title, star三个属性进行检测, 此时三个属性都没有变化, star没有发生变化,是因为实质上在对star检测时只检测star本身的引用值是否发生了改变,改变star的属性值并未改变star本身的引用,因此是没有发生变化。

而当我们点击第二个按钮改变明星对象时 ,重新new了一个 star ,这时变化检测才会检测到 star发生了改变。

然后变化检测进入到子组件中,检测到star.firstName和star.lastName发生了变化, 然后更新视图.

OnPush策略

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

changeDetection:ChangeDetectionStrategy.OnPush
此时, 当点击第一个按钮时, 检测到star没有发生变化, ok,变化检测到此结束, 不会进入到子组件中, 视图不会发生变化.

当点击第二个按钮时,检测到star发生了变化, 然后变化检测进入到子组件中,检测到star.firstName和star.lastName发生了变化, 然后更新视图.

所以,当你使用了OnPush检测机制时,在修改一个绑定值的属性时,要确保同时修改到了绑定值本身的引用。但是每次需要改变属性值的时候去new一个新的对象会很麻烦,immutable.js 你值得拥有!

变化检测对象引用

通过引用变化检测对象ChangeDetectorRef,可以手动去操作变化检测。我们可以在组件中的通过依赖注入的方式来获取该对象:

constructor(
  private changeRef:ChangeDetectorRef
 ){}
登入後複製

变化检测对象提供的方法有以下几种:

  1. markForCheck() - 在组件的 metadata 中如果设置了 changeDetection:ChangeDetectionStrategy.OnPush 条件,那么变化检测不会再次执行,除非手动调用该方法, 该方法的意思是在变化监测时必须检测该组件。

  2. detach() - 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。

  3. reattach() - 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测

  4. detectChanges() - 从该组件到各个子组件执行一次变化检测

OnPush策略下手动发起变化检测

组件中添加事件改变输入属性

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

@Component({
 selector: &#39;movie&#39;,
 styles: [&#39;p {border: 1px solid black}&#39;],
 template: `
<p>
<h3>{{ title }}</h3>
<p>
<button (click)="changeStar()">点击切换名字</button>    
<label>Star:</label>
<span>{{star.firstName}} {{star.lastName}}</span>
</p>
</p>`,
changeDetection:ChangeDetectionStrategy.OnPush
})
export class MovieComponent {
 constructor(
  private changeRef:ChangeDetectorRef
 ){}
 @Input() title: string;
 @Input() star;
 
 changeStar(){
  this.star.lastName = &#39;xjl&#39;;
 }
}
登入後複製

此时点击按钮切换名字时,star更改如下

![图片描述][3]

第二种就是上面讲到的使用变化检测对象中的 markForCheck()方法.

ngOnInit() {
  setInterval(() => {
   this.star.lastName = &#39;xjl&#39;;
   this.changeRef.markForCheck();
  }, 1000);
 }
登入後複製

输入属性为Observable

修改app.component.ts

@Component({
 selector: &#39;app-root&#39;,
 template: `
 <h1>变更检测策略</h1>
 <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 = &#39;change detection&#39;;
 title: string = &#39;OnPush 策略&#39;;
 star: Star = new Star(&#39;周&#39;, &#39;杰伦&#39;);
 count:Observable<any>;

 ngOnInit(){
  this.count = Observable.timer(0, 1000)
 }
 changeStar() {
  this.star.firstName = &#39;吴&#39;;
  this.star.lastName = &#39;彦祖&#39;;
 }
 changeStarObject() {
  this.star = new Star(&#39;刘&#39;, &#39;德华&#39;);
 } 
}
登入後複製

此时,有两种方式让MovieComponent进入检测,一种是使用变化检测对象中的 markForCheck()方法.

ngOnInit() {
  this.addCount.subscribe(() => {
   this.count++;
   this.changeRef.markForCheck();
  })
登入後複製

另外一种是使用async pipe 管道

@Component({
 selector: &#39;movie&#39;,
 styles: [&#39;p {border: 1px solid black}&#39;],
 template: `
<p>
<h3>{{ title }}</h3>
<p>
<button (click)="changeStar()">点击切换名字</button>    
<label>Star:</label>
<span>{{star.firstName}} {{star.lastName}}</span>
</p>
<p>{{addCount | async}}</p>
</p>`,
 changeDetection: ChangeDetectionStrategy.OnPush
})
登入後複製

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

利用vue实现如何在表格里,取每行的id(详细教程)

利用vue移动端UI框架如何实现QQ侧边菜单(详细教程)

使用vue及element组件的安装教程(详细教程)

以上是詳細解讀Angular系列中變化檢測問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板