本文學習總結於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應用中,模板指的的是@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會發生一次數據檢測,從根組件遍歷每個葉子組件,該過程是單向的。
一旦檢測到組件數據狀態改變,就從新調 DOM ompoent render渲染,把數據模型轉換成DOM數據結構(),該數據流是單向的。
在Angular中,單向數據流規則指數據模型發生變化,Angular變動檢測,調用渲染器把應用的數據模型轉化爲DOM數據結構(視圖模型)的過程當中是單向的,不可發生其餘改變的方向。
即Angular從組件樹的頂部到底部的整個渲染掃描過程都是單向的。
咱們但願確保在將數據轉換爲視圖的過程當中,不會進一步修改數據。數據從組件類流向表明它們的DOM數據結構,生成這些DOM數據結構的行爲自己不會對數據進行進一步修改。但在Angular的變動檢測週期中,組件的生命週期鉤子會被調用,這意味着咱們編寫的代碼在該過程當中被調用,該代碼有可能引起數據狀態發生改變。
例如
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查看不一致的錯誤。
變化檢測前:
變化檢測時:
每次變化檢測都是從根組件開始,從上往下執行,遍歷每一個組件。
因爲框架已經自動爲模版生成的代碼作了很是多的優化,即便是在未使用優化過的 model 的狀況下都已經能夠達到 ng 1髒檢測性能的 3-10 倍(一樣綁定數量,一樣檢測次數)。視頻中提到了這是由於 ng 2 在生成模版代碼時,會動態生成讓 js 引擎易於優化的代碼,大概原理就是保持每次 check change 先後對象「形狀 」的一致。而若是在性能有瓶頸的地方,可使用下面兩種方式進行高階優化:
OnPush變化檢測策略+Immutable
OnPush變化檢測策略+Observable
OnPush 策略:若輸入屬性沒有發生變化,組件的變化檢測將會被跳過
Angular對複雜數據類型即對象的檢測只是檢測對該對象的引用是否改變
當對象屬性值改變,但對其引用沒改變,Angular會默該改數據沒發生變化。
實踐例子可參考:Angular 2 Change Detection - 2 的OnPush策略章節。
所以當咱們使用 OnPush 策略時,須要使用的 Immutable 的數據結構(Immutable 即不可變,表示當數據模型發生變化的時候,咱們不會修改原有的數據模型,而是建立一個新的數據模型),才能保證程序正常運行。
爲了提升變化檢測的性能,咱們應該儘量在組件中使用 OnPush 策略,爲此咱們組件中所需的數據,應僅依賴於輸入屬性。
使用 immutable 時 change detection cycle 依舊從 root component 開始往下,依次檢測。
上圖每一個 component 都使用了 immutable model ,白色的部分是變動的部分,則在一個 change detection cycle 中只會 recheck & render 白色的部分,從而大大減小處理變動的代價。
而使用 OnPush變化檢測策略 和 Observable 時,狀況就不同了,它的變動極可能是從一個很是下層的子 component 中開始發生的,好比:
在圖中一個子 component 經過 observable 觀察到了一次數據的變動。這個時候咱們須要告知 Angular 這個部分發生了變動,它將會把這個 component 與它的父 component 一直到 root component 標記出來,並單獨檢測這一部分的變動:
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 實例提供的方法,能讓你更靈活地控制檢測器的行爲,最終提升系統的總體性能。