Angular開發實踐(五):深刻解析變化監測

什麼是變化監測

在使用 Angular 進行開發中,咱們經常使用到 Angular 中的綁定——模型到視圖的輸入綁定、視圖到模型的輸出綁定以及視圖與模型的雙向綁定。而這些綁定的值之因此能在視圖與模型之間保持同步,正是得益於Angular中的變化檢測。javascript

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

變化監測的源頭

變化監測的關鍵在於如何最小粒度地監測到綁定的值是否發生了改變,那麼在什麼狀況下會致使這些綁定的值發生變化呢?咱們能夠看一下咱們經常使用的幾種場景:web

Events: click/hover/...

@Component({
  selector: 'demo-component',
  template: `
    <h1>{{name}}</h1>
    <button (click)="changeName()">change name</button>
  `
})
export class DemoComponent {
    name: string = 'Tom';
    
    changeName() {
        this.name = 'Jerry';
    }
}

咱們在模板中經過插值表達式綁定了 name 屬性。當點擊change name按鈕時,改變了 name 屬性的值,這時模板視圖顯示內容也發生了改變。服務器

XHR/webSocket

@Component({
  selector: 'demo-component',
  template: `
    <h1>{{name}}</h1>
  `
})
export class DemoComponent implements OnInit {
    name: string = 'Tom';
    
    constructor(public http: HttpClient) {}
    
    ngOnInit() {
        // 假設有這個./getNewName請求,返回一個新值'Jerry'
        this.http.get('./getNewName').subscribe((data: string) => {
            this.name = data;
        });
    }
}

咱們在這個組件的 ngOnInit 函數裏向服務器端發送了一個 Ajax 請求,當這個請求返回結果時,一樣會改變當前模板視圖上綁定的 name 屬性的值。異步

Times: setTimeout/requestAnimationFrame

@Component({
  selector: 'demo-component',
  template: `
    <h1>{{name}}</h1>
  `
})
export class DemoComponent implements OnInit {
    name: string = 'Tom';
    
    constructor() {}
    
    ngOnInit() {
        // 假設有這個./getNewName請求,返回一個新值'Jerry'
        setTimeout(() => {
            this.name = 'Jerry';
        }, 1000);
    }
}

咱們在這個組件的ngOnInit函數裏經過設定一個定時任務,當定時任務執行時,一樣會改變當前視圖上綁定的name屬性的值。函數

總結

  • 其實,咱們不難發現上述三種狀況都有一個共同點,即這些致使綁定值發生改變的事件都是異步發生的。學習

  • Angular並非捕捉對象的變更,它採用的是在適當的時機去檢驗對象的值是否被改動,這個時機就是這些異步事件的發生。this

  • 這個時機是由 NgZone 這個服務去掌控的,它獲取到了整個應用的執行上下文,可以對相關的異步事件發生、完成或者異常等進行捕獲,而後驅動 Angular 的變化監測機制執行。spa

變化監測的處理機制

經過上面的介紹,咱們大體明白了變化檢測是如何被觸發的,那麼 Angular 中的變化監測是如何執行的呢?雙向綁定

首先咱們須要知道的是,對於每個組件,都有一個對應的變化監測器;即每個 Component 都對應有一個changeDetector,咱們能夠在 Component 中經過依賴注入來獲取到changeDetector

而咱們的多個 Component 是一個樹狀結構的組織,因爲一個 Component 對應一個changeDetector,那麼changeDetector之間一樣是一個樹狀結構的組織。

最後咱們須要記住的一點是,每次變化監測都是從 Component 樹根開始的。

舉個例子

子組件:

@Component({
  selector: 'demo-child',
  template: `
    <h1>{{title}}</h1>
    <p>{{paramOne}}</p>
    <p>{{paramTwo}}</p>
  `
})
export class DemoChildComponent {
    title: string = '子組件標題';
    @Input() paramOne: any; // 輸入屬性1
    @Input() paramTwo: any; // 輸入屬性2
}

父組件:

@Component({
  selector: 'demo-parent',
  template: `
    <h1>{{title}}</h1>
    <demo-child [paramOne]='paramOneVal' [paramTwo]='paramTwoVal'></demo-child>
    <button (click)="changeVal()">change name</button>
  `
})
export class DemoParentComponent {
    title: string = '父組件標題';
    paramOneVal: any = '傳遞給paramOne的數據';
    paramTwoVal: any = '傳遞給paramTwo的數據';
    
    changeVal() {
        this.paramOneVal = '改變以後的傳遞給paramOne的數據';
    }
}

上面的代碼中,DemoParentComponent 經過 <demo-child></demo-child> 標籤嵌入了 DemoChildComponent,從樹狀結構上來講,DemoParentComponent 是 DemoChildComponent 的根節點,而 DemoChildComponent 是 DemoParentComponent 的葉子節點。

當咱們點擊 DemoParentComponent 的 button 時,會回調到 changeVal 方法,而後會觸發變化監測的執行,變化監測流程以下:

首先變化檢測從 DemoParentComponent 開始:

  • 檢測 title 值是否發生了變化:沒有發生變化

  • 檢測 paramOneVal 值是否發生了變化:發生了變化(點擊按鈕調用changeVal()方法改變的)

  • 檢測 paramTwoVal 值是否發生了變化:沒有發生變化

而後變化檢測進入到葉子節點 DemoChildComponent:

  • 檢測 title 值是否發生了改變:沒有發生變化

  • 檢測 paramOne 是否發生了變化:發生了改變(因爲父組件的屬性paramOneVal發生了改變)

  • 檢測 paramTwo 是否發生了改變:沒有發生變化

最後,由於 DemoChildComponent 再也沒有了葉子節點,因此變化監測將更新DOM,同步視圖與模型之間的變化。

變化監測策略

學習了變化監測的處理機制以後,你可能會想,這機制未免也有點太簡單粗暴了吧,假如個人應用中有成百上千個 Component,隨便一個 Component 觸發了監測,那麼都須要從根節點到葉子節點從新檢測一遍。

彆着急,Angular 的開發團隊已經考慮到了這個問題,上述的檢測機制只是一種默認的檢測機制,Angular 還提供一種 OnPush 的檢測機制(設置元數據屬性 changeDetection: ChangeDetectionStrategy.OnPush)。

OnPush 與 Default 之間的差異:當檢測到與子組件輸入綁定的值沒有發生改變時,變化檢測就不會深刻到子組件中去

變化監測類 - ChangeDetectorRef

上面說到咱們能夠修改組件元數據屬性 changeDetection 來修改組件的變化監測策略(ChangeDetectionStrategy.Default 或 ChangeDetectionStrategy.OnPush),除了這個,咱們還可使用 ChangeDetectorRef 來更加靈活的控制組件的變化監測。

Angular 在整個運行期間都會爲每個組件建立 ChangeDetectorRef 的實例,該實例提供了相關方法來手動管理變化監測。有了這個類,咱們本身就能夠自定義組件的變化監測策略了,如中止/啓用變化監測或者按指定路徑變化監測等等。

相關方法以下:

  • markForCheck():把根組件到該組件之間的這條路徑標記起來,通知Angular在下次觸發變化監測時必須檢查這條路徑上的組件。

  • detach():從變化監測樹中分離變化監測器,該組件的變化監測器將再也不執行變化監測,除非再次手動執行reattach()方法。

  • reattach():把分離的變化監測器從新安裝上,使得該組件及其子組件都能執行變化監測。

  • detectChanges():手動觸發執行該組件到各個子組件的一次變化監測。

使用方法也很簡單,直接在組件中注入便可:

@Component({
  selector: 'demo-parent',
  template: `
    <h1>{{title}}</h1>
  `
})
export class DemoParentComponent implements OnInit {
    title: string = '組件標題';
    
    constructor(public cdRef: ChangeDetectorRef) {}
    
    ngOnInit() {
        this.cdRef.detach(); // 中止組件的變化監測,看需求使用不一樣的方法
    }
}
相關文章
相關標籤/搜索