Angular 2.x+ 髒檢查機制理解

Angular 2.x+ 髒檢查機制理解

目前幾種主流的前端框架都已經實現雙向綁定特性,但實現的方法各有不一樣:javascript

  • 發佈者-訂閱者模式(backbone.js)
  • 髒值檢查(angular.js)
  • 數據劫持 + 發佈者-訂閱者模式(vue.js)

下面咱們就來了解一下 ng2.x+ 的版本中的髒檢查機制是如何運行的。html

什麼是變化檢測

變化檢測(髒檢查)的基本任務是獲取程序內部狀態的變化,並使其在用戶界面上以某種方式可見,這種狀態的變化能夠來自於 JavaScript 的任何數據結構,最終呈現爲用戶界面中的段落、表單、連接或者按鈕等 DOM 對象。咱們把輸入數據結構並生成 DOM 結構顯示給用戶的過程叫做 渲染

然而在程序運行時發生變化狀況比較複雜,咱們須要肯定模型中發生什麼變化,以及什麼地方須要更新 DOM 節點。操做 DOM 樹十分昂貴,因此咱們不只須要找出待更新的地方,還須要保持操做數儘量小。這可能經過許多不一樣的方式來解決:好比咱們能夠簡單的發起 http 請求並從新渲染整個頁面,或者能夠區分 DOM 樹的新舊狀態並只從新渲染兩者不一樣的部分(ReactJS 虛擬 DOM 的解決方案)。前端

什麼會引發變化

何時會產生變化?
Angular 如何確知更新視圖的時機?
@Component({
  template: `
    <h1>{{firstname}} {{lastname}}</h1>
    <button (click)="changeName()">Change name</button>
  `
})

class MyApp {
  firstname:string = 'Pascal';
  lastname:string = 'Precht';

  changeName() {
    this.firstname = 'Brad';
    this.lastname = 'Green';
  }
}
上面的組件只是顯示兩個屬性,並提供一個方法來改變他們(點擊模板中的按鈕),點擊這個特定按鈕的時刻便是應用程序狀態發生改變的時刻,由於它改變組件的屬性,這也是咱們想要更新視圖的時刻。
@Component()
class ContactsApp implements OnInit{
  contacts:Contact[] = [];

  constructor(private http: Http) {}

  ngOnInit() {
    this.http.get('/contacts')
      .map(res => res.json())
      .subscribe(contacts => this.contacts = contacts);
  }
}
這個組件擁有一個聯繫人列表,當它初始化時發送一個 http 請求,一旦這個請求返回列表就會被更新,此時咱們的應用程序狀態發生改變,並須要更新視圖。

基本上應用程序狀態的改變能夠由三類活動引發:vue

  1. 事件 - click, submit, ...
  2. XHR - 從遠程服務器獲取數據
  3. 定時器 - setTimeout, setInterval
上述活動都是異步的,所以咱們能夠得出結論:每當執行一些異步操做時,咱們的應用程序狀態可能發生改變,這時則須要 Angular 更新視圖。

Angular 在啓動時會重寫(經過 Zone.js)部分底層瀏覽器 APIs 好比 addEventListenerjava

// this is the new version of addEventListener
function addEventListener(eventName, callback) {
     // call the real addEventListener
     callRealAddEventListener(eventName, function() {
        // first call the original callback
        callback(...);     
        // and then run Angular-specific functionality
        var changed = angular2.runChangeDetection();
         if (changed) {
             angular2.reRenderUIPart();
         }
     });
}

誰通知 Angular 更新視圖

Zone 負責通知 Angular 進行視圖更新,Angular 封裝有 NgZone,簡單來講,經過 Angular 的部分源碼咱們能夠知道有一個叫做 ApplicationRef 的東西負責監聽 NgZone 中的 onTurnDone 事件,每當該事件觸發時,它就執行 trick 方法進行變化檢測的基本工做。json

// very simplified version of actual source
class ApplicationRef {
  changeDetectorRefs:ChangeDetectorRef[] = [];

  constructor(private zone: NgZone) {
    this.zone.onTurnDone
      .subscribe(() => this.zone.run(() => this.tick());
  }

  tick() {
    this.changeDetectorRefs
      .forEach((ref) => ref.detectChanges());
  }
}

變化檢測

首先咱們須要注意的是在 Angular 中每一個組件都有本身的變化檢測器,這使得咱們能夠對每一個組件分別控制如何以及什麼時候進行變化檢測。

Change Detection.jpg-99.3kB

因爲每一個組件都有其本身的變化檢測器,即一個 Angular 應用程序由一個組件樹組成,因此邏輯結果就是咱們也有一個變化檢測器樹,這棵樹也能夠看做是一個有向圖,數據老是從上到下流動。

數據從上到下的緣由是由於變化檢測也老是從上到下對每個單獨的組件進行,每一次從根組件開始,單向數據流比循環髒檢查更可預測,咱們老是能夠知道視圖中使用的數據來自哪裏。瀏覽器

Change Detection 2.jpg-131.4kB

咱們假設在組件樹的某個地方觸發一個事件,好比一個按鈕被點擊,zones 會進行事件的處理並通知 Angular,而後變化檢測依次向下傳遞。

如何觸發變化檢測

一種方法是基於組件的生命週期鉤子:
ngAfterViewChecked() {
    if (this.callback && this.clicked) {
        console.log("changing status ...");
        this.callback(Math.random());
    }
}
在開發模式下運行 Angular 會在控制檯中獲得一條錯誤日誌,生產模式下則不會拋出。
EXCEPTION: Expression '{{message}} in App@3:20' has changed after it was checked
另外一種方法是手動控制變化檢測的打開/關閉,並手動觸發:
constructor(private ref: ChangeDetectorRef) {
    ref.detach();
    setInterval(() => {
      this.ref.detectChanges();
    }, 5000);
  }

改善的髒檢查

Angular 2.x+ 的數據流是自頂向下,從父組件向子組件的的單向流動,變化監測樹與之相呼應,單項數據量保證變化監測的高效性和可預測性。檢查父組件後,子組件可能會改變父組件中的數據使得父組件須要被再次檢查,這是不被推薦的數據處理方式,而且在開發模式下這種狀況會拋出異常 ExpressionChangedAfterItHasBeenCheckedError,在生產模式下不會報錯可是髒檢查僅會執行一次。

相比之下 1.x 的版本採用雙向數據流,爲了使得數據最終趨向穩定不得很少次檢查錯綜複雜的數據流,性能提高就此可見一斑。前端框架

性能

默認狀況下,即便每次發生事件都須要檢查每一個組件,Angular 速度仍然很是快,它能夠在幾毫秒內執行數十萬次檢查,這主要是因爲 Angular 能夠生成 VM 友好的代碼。

更優的變化檢測

Angular 每次都要檢查每一個組件,由於事件發生的緣由也許是應用程序狀態已經改變,可是若是咱們可以告訴 Angular 只對那些改變狀態的應用程序部分運行變化檢測,那不是很好嗎?

事實證實,有些數據結構能夠給咱們何時發生變化的一些保證 - ImmutablesObservables服務器

理解不可變

好比咱們擁有一個組件 VCardApp 使用 v-card 做爲子組件,其具備一個輸入屬性 vData,而且咱們可使用 changeData 方法改變 vData 對象的 name 屬性(並不會改變該對象的引用)。
@Component({
  template: '<v-card [vData]="vData"></v-card>'
})

class VCardApp {
  constructor() {
    this.vData = {
      name: 'Christoph Burgdorf',
      email: 'christoph@thoughtram.io'
    }
  }

  changeData() {
    this.vData.name = 'Pascal Precht';
  }
}
當某些事件致使 changeData 執行時, vData.name 發生改變並傳遞至 v-card 中, v-card 組件的變化檢測器檢查給定的數據新 vData 是否與之前同樣,在數據引用未變可是其參數改變的狀況下,Angular 也須要對該數據進行變化監測。

這就是 immutable 數據結構發揮做用的地方。angular2

[
How I optimized Minesweeper using Angular 2 and Immutable.js to make it insanely fast](https://www.jvandemo.com/how-...

不可變對象

Immutable 爲咱們提供不可變的對象:這意味着若是咱們使用不可變的對象,而且想要對這樣的對象進行更改,咱們會獲得一個新的引用(保證原始對象不變)。
var vData = someAPIForImmutables.create({
              name: 'Pascal Precht'
            });

var vData2 = vData.set('name', 'Christoph Burgdorf');

vData === vData2 // false
上述僞代碼即演示不可變對象的含義,其中 someAPIForImmutables 能夠是咱們想要用於不可變數據結構的任何 API。

OnPush 策略減小檢測次數

當輸入屬性不變時,Angular能夠跳過整個變動檢測子樹。若是咱們在 Angular 應用程序中使用不可變對象,咱們所須要作的就是告訴 Angular 組件能夠跳過變化檢測,若是它的輸入沒有改變的話。
@Component({
  template: `
    <h2>{{vData.name}}</h2>
    <span>{{vData.email}}</span>
  `
})

class VCardCmp {
  @Input() vData;
}
正如咱們所看到的, VCardCmp 只依賴於它的輸入屬性,咱們能夠告訴 Angular 跳過這個組件的子樹的變化檢測,若是它的輸入沒有改變,經過設置變化檢測策略 OnPush 是這樣的:
@Component({
  template: `
    <h2>{{vData.name}}</h2>
    <span>{{vData.email}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})

class VCardCmp {
  @Input() vData;
}

Change Detection 3.png-123.2kB

Angular OnPush Change Detection and Component Design - Avoid Common Pitfalls

Observables

與不可變的對象不一樣,當進行更改時 Observables 不會給咱們提供新的引用,而是發射咱們能夠訂閱的事件來對他們作出反應。
@Component({
  template: '{{counter}}',
  changeDetection: ChangeDetectionStrategy.OnPush
})

class CartBadgeCmp {
  @Input() addItemStream:Observable<any>;
  counter = 0;

  ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++; // application state changed
    })
  }
}
好比咱們用購物車創建一個電子商務應用程序:每當用戶將產品放入購物車時,咱們須要在用戶界面中顯示一個小計數器,以便用戶能夠看到購物車中的產品數量。

該組件有一個 counter 屬性和一個輸入屬性 addItemStream,當產品被添加到購物車時,這是一個被觸發的事件流。另外,咱們設置了變化檢測策略 OnPush,只有當組件的輸入屬性發生變化時,變化檢測纔會執行。

如前所述,引用 addItemStream 永遠不會改變,因此組件的子樹從不執行變動檢測。

Change Detection 4.jpg-89.6kB

當整個樹被設置成 OnPush 後,咱們如何通知 Angular 須要對這個組件進行變化檢測呢?正如咱們所知,變化檢測老是從上到下執行的,因此咱們須要的是一種能夠檢測樹的整個路徑到發生變化的組件的變化的方法。

咱們能夠經過依賴注入訪問組件的 ChangeDetectorRef,這個注入來自一個叫作 markForCheck 的 API,它標記從組件到根的路徑,以便下次更改檢測的運行。

constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++; // application state changed
      this.cd.markForCheck(); // marks path
    })
  }
}
下面是在可觀察事件被觸發後,變化檢測開始前。

Change Detection 5.jpg-87.5kB

如今當執行更改檢測時,它將從上到下進行。

Change Detection 6.jpg-103.8kB

而且一旦更改檢測運行結束,它將恢復 OnPush 整個樹的狀態。

TAKING ADVANTAGE OF OBSERVABLES IN ANGULAR

ANGULAR CHANGE DETECTION EXPLAINED
How does Angular Change Detection Really Work ?
Change And Its Detection In JavaScript Frameworks

相關文章
相關標籤/搜索