Angular 變動檢測及單向數據流

本文學習總結於segmentfault

數據

在Angular中,咱們所說的數據,即組件持有的數據模型。數據結構

import { Component } from '@angular/core';
import {Course} from "./course";

export interface Course {
    id:number;
    description:string;
}

@Component({
  selector: 'app-root',
  template: `
    <div class="course">
        <span class="description">{{course.description}}</span>
    </div>
`})
export class AppComponent {


    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };
    
}

Angular經過模板把數據模型轉換成視圖。app

模板在Angular內部的使用

Angular應用中,模板指的的是@Component裝飾器的template或templateUrl指向的HTML頁面
例如:框架

@Component({
  selector: 'app-root',
  template: `
    <div class="course">
        <span class="description">{{course.description}}</span>
    </div>
`})
export class AppComponent {


    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };
    
}

在模板出現問題時,咱們會收到有用的錯誤消息。 因此很明顯Angular不是簡單地用一個字符串來處理模板。 那麼這是如何工做的?
在Angular中,Angular並非把數據經過替換一些變量來建立基於模板的實際HTML而後將此HTML傳遞給瀏覽器而後瀏覽器解析HTML並生成DOM數據結構瀏覽器渲染引擎而後在屏幕上呈現視圖。dom

Angular不會生成HTML字符串,它直接生成DOM數據結構。異步

實際上,Angular把取數據模型應用於一個函數(DOM component renderer)。 該函數的輸出是對應於此HTML模板的DOM數據結構。函數

該函數的定義大致以下:

View_AppComponent_0.prototype.createInternal = function(rootSelector) {
        var self = this;
        
        var parentRenderNode = self.renderer.createViewRoot(self.parentElement);
        
        self._text_0 = self.renderer.createText(parentRenderNode,'\n',self.debug(0,0,0));
        
        self._el_1 = jit_createRenderElement5(self.renderer,parentRenderNode,'div',
               new jit_InlineArray26(2,'class','course'),self.debug(1,1,0));
        
        self._text_2 = self.renderer.createText(self._el_1,'\n\n    ',self.debug(2,1,20));
        
        self._el_3 = jit_createRenderElement5(self.renderer,self._el_1,'span',
              new jit_InlineArray26(2,'class','description'),self.debug(3,3,4));
        
        self._text_4 = self.renderer.createText(self._el_3,'',self.debug(4,3,30));
        
        self._text_5 = self.renderer.createText(self._el_1,'\n\n',self.debug(5,3,59));
        
        self._text_6 = self.renderer.createText(parentRenderNode,'\n',self.debug(6,5,6));
        
        self.init(null,(self.renderer.directRenderer? null: [
                self._text_0,
                self._el_1,
                self._text_2,
                self._el_3,
                self._text_4,
                self._text_5,
                self._text_6
            ]
        ),null);
        return null;
    };

從其中createViewRoot,createText一些方法和parentElement,parentRenderNode命名,能夠大概知道該函數正在建立一個DOM數據。

一旦數據狀態發生改變,Angular數據檢測器檢測到,將從新調用
該DOM component renderer。

如何查看本身的組件的DOM component renderer,以及該函數的產生時機,請參考原文中的Where can I find this function for my components ?When is this code generated ?

引發數據模型變化的來源

數據模型一旦發生改變,視圖就要相應發生變化,這也是如今流行的Model Driven View。那麼就客戶端(瀏覽器)來講,引發數據模型發生變化的事件源有:

  • Events:click, mouseover, keyup ...

  • Timers:setInterval、setTimeout

  • XHRs:Ajax(GET、POST ...)

這些事件源有一個共同的特性,即它們都是異步操做。那咱們能夠這樣認爲,全部的異步操做都有可能會引發模型的變化。

變動檢測和單向數據流規則

每個異步操做都有可能引發數據狀態的變動, Angular封裝 Zone來攔截跟蹤異步。

  • 每一次異步操做後Angular會發生一次數據檢測,從根組件遍歷每個葉子組件,該過程是單向的。
    image

  • 一旦檢測到組件數據狀態改變,就從新調 DOM ompoent render渲染,把數據模型轉換成DOM數據結構(),該數據流是單向的。

在Angular中,單向數據流規則指數據模型發生變化,Angular變動檢測,調用渲染器把應用的數據模型轉化爲DOM數據結構(視圖模型)的過程當中是單向的,不可發生其餘改變的方向。

image

即Angular從組件樹的頂部到底部的整個渲染掃描過程都是單向的。

爲何要單向數據流?

咱們但願確保在將數據轉換爲視圖的過程當中,不會進一步修改數據。數據從組件類流向表明它們的DOM數據結構,生成這些DOM數據結構的行爲自己不會對數據進行進一步修改。但在Angular的變動檢測週期中,組件的生命週期鉤子會被調用,這意味着咱們編寫的代碼在該過程當中被調用,該代碼有可能引起數據狀態發生改變。

image

例如

import {Component, AfterViewChecked} from '@angular/core';
import {Course} from "./course";

@Component({
    selector: 'app-root',
    template: `
    <div class="course">
        <span class="description">{{course.description}}</span>
    </div>
`})
export class AppComponent implements AfterViewChecked {

    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };

    ngAfterViewChecked() {
        this.course.description += Math.random();
    }

}

上述代碼會在Angular變動檢測週期發生錯誤。咱們在該組件ngAfterViewChecked()方法中修改了數據狀態。致使了視圖渲染後,數據跟視圖狀態不一致。

解決:

ngAfterViewChecked() {
    setTimeout(() => {
        this.course.description += Math.random();
    });
}

咱們可使用setTimeout將數據修改延遲到下一個變動週期。

除了組件生命週期回調的鉤子可能觸發數據狀態的改變還有其餘,
例如

import { Component } from '@angular/core';
import {Course} from "./course";

@Component({
    selector: 'app-root',
    template: `
    <div class="course">
        <span class="description">{{description}}</span>
    </div>
`})
export class AppComponent {

    course: Course = {
        id: 1,
        description: "Angular For Beginners"
    };

    get description() {
        return this.course.description + Math.random();
    }
}

Angular每次檢測description時,它都會返回一個不一樣值。

在Angular變動檢測週期,任意會改變數據狀態的行爲都會拋出異常從而終止。

若是Angular沒有制止該行爲,數據和視圖會保持在不一致的狀態,其中渲染過程完成後的視圖不反映數據的實際狀態。或者重複檢測,直到數據穩定可能會致使性能問題。

單向數據流重要性

  • 首先是由於它有助於從渲染過程當中得到很好的性能。

  • 它確保了當咱們的事件處理程序返回而且框架接管渲染結果時,沒有什麼不可預測的發生。

  • 防止數據vs查看不一致的錯誤。

變化檢測性能優化

變化檢測前:

image

變化檢測時:

image

每次變化檢測都是從根組件開始,從上往下執行,遍歷每一個組件。
因爲框架已經自動爲模版生成的代碼作了很是多的優化,即便是在未使用優化過的 model 的狀況下都已經能夠達到 ng 1髒檢測性能的 3-10 倍(一樣綁定數量,一樣檢測次數)。視頻中提到了這是由於 ng 2 在生成模版代碼時,會動態生成讓 js 引擎易於優化的代碼,大概原理就是保持每次 check change 先後對象「形狀 」的一致。而若是在性能有瓶頸的地方,可使用下面兩種方式進行高階優化:

  • OnPush變化檢測策略+Immutable

  • OnPush變化檢測策略+Observable

OnPush變化檢測策略

OnPush 策略:若輸入屬性沒有發生變化,組件的變化檢測將會被跳過

OnPush變化檢測策略+Immutable

Angular對複雜數據類型即對象的檢測只是檢測對該對象的引用是否改變

當對象屬性值改變,但對其引用沒改變,Angular會默該改數據沒發生變化。

實踐例子可參考:Angular 2 Change Detection - 2 的OnPush策略章節。

所以當咱們使用 OnPush 策略時,須要使用的 Immutable 的數據結構(Immutable 即不可變,表示當數據模型發生變化的時候,咱們不會修改原有的數據模型,而是建立一個新的數據模型),才能保證程序正常運行。

爲了提升變化檢測的性能,咱們應該儘量在組件中使用 OnPush 策略,爲此咱們組件中所需的數據,應僅依賴於輸入屬性

OnPush變化檢測策略+Observable

使用 immutable 時 change detection cycle 依舊從 root component 開始往下,依次檢測。
image
上圖每一個 component 都使用了 immutable model ,白色的部分是變動的部分,則在一個 change detection cycle 中只會 recheck & render 白色的部分,從而大大減小處理變動的代價。

而使用 OnPush變化檢測策略 和 Observable 時,狀況就不同了,它的變動極可能是從一個很是下層的子 component 中開始發生的,好比:
image
在圖中一個子 component 經過 observable 觀察到了一次數據的變動。這個時候咱們須要告知 Angular 這個部分發生了變動,它將會把這個 component 與它的父 component 一直到 root component 標記出來,並單獨檢測這一部分的變動:

image

ChangeDetectorRef

ChangeDetectorRef 是組件的變化檢測器的引用,咱們能夠在組件中的經過依賴注入的方式來獲取該對象,來手動控制組件的變化檢測行爲:

ChangeDetectorRef 變化檢測類中主要方法有如下幾個:

export abstract class ChangeDetectorRef {
  abstract markForCheck(): void;
  abstract detach(): void;
  abstract detectChanges(): void;
  abstract reattach(): void;
}

其中各個方法的功能介紹以下:

  • markForCheck() - 在組件的 metadata 中若是設置了 changeDetection: ChangeDetectionStrategy.OnPush 條件,那麼變化檢測不會再次執行,除非手動調用該方法。

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

  • reattach() - 從新添加已分離的變化檢測器,使得該組件及其子組件都能執行變化檢測

  • detectChanges() - 從該組件到各個子組件執行一次變化檢測

import { Component, Input, OnInit, ChangeDetectionStrategy, 
         ChangeDetectorRef } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Component({
    selector: 'exe-counter',
    template: `
      <p>當前值: {{ counter }}</p>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
    counter: number = 0;

    @Input() addStream: Observable<any>;

    constructor(private cdRef: ChangeDetectorRef) { }

    ngOnInit() {
        this.addStream.subscribe(() => {
            this.counter++;
            this.cdRef.markForCheck();
        });
    }
}

總結

  • Angular變動檢測週期,數據從組件類到DOM,遵循着單向數據流的規則。

  • Angular是一顆有向樹,默認狀況下,變化檢測系統將會走遍整棵樹,但咱們可使用 OnPush 變化檢測策略,使用Immutable 能解決大部分問題,而使用Observables 對象,進而利用 ChangeDetectorRef 實例提供的方法,能讓你更靈活地控制檢測器的行爲,最終提升系統的總體性能。

相關文章
相關標籤/搜索