更新時間 - 2017-03-20 16:15;
更新內容 - 我有話說模塊javascript
在 Angular 2 Change Detection - 1 文章中,咱們介紹了瀏覽器渲染、Zone、NgZone 的概念,本文將詳細介紹 Angular 2 組件中的變化檢測器。java
如你所知,Angular 2 應用程序是一顆組件樹,而每一個組件都有本身的變化檢測器,這意味着應用程序也是一顆變化檢測器樹。順便說一句,你可能會想。是由誰來生成變化檢測器?這是個好問題,它們是由代碼生成。 Angular 2 編譯器爲每一個組件自動建立變化檢測器,並且最終生成的這些代碼 JavaScript VM友好代碼。這也是爲何新的變化檢測是快速的 (相比於 Angular 1.x 的 $digest)。基本上,每一個組件能夠在幾毫秒內執行數萬次檢測。所以你的應用程序能夠快速執行,而無需調整性能。typescript
另外在 Angular 2 中,任何數據都是從頂部往底部流動,即單向數據流。下圖是 Angular 1.x 與 Angular 2 變化檢測的對比圖:segmentfault
讓咱們來看一下具體例子:瀏覽器
child.component.ts性能優化
import { Component, Input } from '@angular/core'; @Component({ selector: 'exe-child', template: ` <p>{{ text }}</p> ` }) export class ChildComponent { @Input() text: string; }
parent.component.ts數據結構
import { Component, Input } from '@angular/core'; @Component({ selector: 'exe-parent', template: ` <exe-child [text]="name"></exe-child> ` }) export class ParentComponent { name: string = 'Semlinker'; }
app.component.tsapp
import { Component } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <exe-parent></exe-parent> ` }) export class AppComponent{ }
變化檢測老是從根組件開始。上面的例子中,ParentComponent 組件會比 ChildComponent 組件更早執行變化檢測。所以在執行變化檢測時 ParentComponent 組件中的 name 屬性,會傳遞到 ChildComponent 組件的輸入屬性 text 中。此時 ChildComponent 組件檢測到 text 屬性發生變化,所以組件內的 p 元素內的文本值從空字符串 變成 'Semlinker' 。這雖然很簡單,但很重要。另外對於單次變化檢測,每一個組件只檢查一次。函數
當組件的任何輸入屬性發生變化的時候,咱們能夠經過組件生命週期提供的鉤子 ngOnChanges
來捕獲變化的內容。具體示例以下:性能
import { Component, Input, OnChanges, SimpleChange } from '@angular/core'; @Component({ selector: 'exe-child', template: ` <p>{{ text }}</p> ` }) export class ChildComponent implements OnChanges{ @Input() text: string; ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.dir(changes['text']); } }
以上代碼運行後,控制檯的輸出結果:
咱們看到當輸入屬性變化的時候,咱們能夠經過組件提供的生命週期鉤子 ngOnChanges
捕獲到變化的內容,即 changes
對象,該對象的內部結構是 key-value
鍵值對的形式,其中 key 是輸入屬性的值,value 是一個 SimpleChange 對象,該對象內包含了 previousValue (以前的值) 和 currentValue (當前值)。
須要注意的是,若是在組件內手動改變輸入屬性的值,ngOnChanges 鉤子是不會觸發的。具體示例以下:
import { Component, Input, OnChanges, SimpleChange } from '@angular/core'; @Component({ selector: 'exe-child', template: ` <p>{{ text }}</p> <button (click)="changeTextProp()">改變Text屬性</button> ` }) export class ChildComponent implements OnChanges { @Input() text: string; ngOnChanges(changes: { [propName: string]: SimpleChange }) { console.dir(changes['text']); } changeTextProp() { this.text = 'Text屬性已改變'; } }
當你點擊 '改變Text屬性' 的按鈕時,發現頁面中 p 元素的內容會從 'Semlinker' 更新爲 'Text屬性已改變' ,但控制檯卻沒有輸出任何信息,這驗證了咱們剛纔給出的結論,即在組件內手動改變輸入屬性的值,ngOnChanges 鉤子是不會觸發的。
在介紹如何優化變化檢測的性能前,咱們先來看幾張圖:
變化檢測前:
變化檢測時:
咱們發現每次變化檢測都是從根組件開始,從上往下執行。雖然 Angular 2 優化後的變化檢測執行的速度很快,但咱們可否只針對那些有變化的組件才執行變化檢測或靈活地控制變化檢測的時機呢 ? 答案是有的,接下來咱們看一下具體怎麼進行優化。
在 Angular 2 中咱們能夠在定義組件的 metadata 信息時,設定每一個組件的變化檢測策略。接下來咱們來看一下具體示例:
profile-name.component.ts
import { Component, Input} from '@angular/core'; @Component({ selector: 'profile-name', template: ` <p>Name: {{name}}</p> ` }) export class ProfileNameComponent { @Input() name: string; }
profile-age.component.ts
import { Component, Input } from '@angular/core'; @Component({ selector: 'profile-age', template: ` <p>Age: {{age}}</p> ` }) export class ProfileAgeComponent { @Input() age: number; }
profile-card.component.ts
import { Component, Input } from '@angular/core'; @Component({ selector: 'profile-card', template: ` <div> <profile-name [name]='profile.name'></profile-name> <profile-age [age]='profile.age'></profile-age> </div> ` }) export class ProfileCardComponent { @Input() profile: { name: string; age: number }; }
app.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <profile-card [profile]='profile'></profile-card> ` }) export class AppComponent { profile: { name: string; age: number } = { name: 'Semlinker', age: 31 }; }
上面代碼中 ProfileCardComponent 組件,有一個 profile
輸入屬性,並且它的模板視圖只依賴於該屬性。若是使用默認的檢測策略,每當發生變化時,都會從根組件開始,從上往下在每一個組件上執行變化檢測。但若是 ProfileCardComponent 中的 profile 輸入屬性沒有發生變化,是沒有必要再執行變化檢測。針對這種狀況,Angular 2 爲咱們提供了 OnPush 的檢測策略。
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'profile-card', template: ` <div> <profile-name [name]='profile.name'></profile-name> <profile-age [age]='profile.age'></profile-age> </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ProfileCardComponent { @Input() profile: { name: string; age: number }; }
當使用 OnPush 策略的時候,若輸入屬性沒有發生變化,組件的變化檢測將會被跳過,以下圖所示:
實踐是檢驗真理的惟一標準,咱們立刻來個例子:
app.component.ts
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <profile-card [profile]='profile'></profile-card> ` }) export class AppComponent implements OnInit{ profile: { name: string; age: number } = { name: 'Semlinker', age: 31 }; ngOnInit() { setTimeout(() => { this.profile.name = 'Fer'; }, 2000); } }
以上代碼運行後,瀏覽器的輸出結果:
咱們發現雖然在 AppComponent 組件中 profile 對象中的 name 屬性已經被改變了,但頁面中名字的內容卻未同步刷新。在進一步分析以前,咱們先來介紹一下 Mutable 和 Immutable 的概念。
在 JavaScript 中默認全部的對象都是可變的,即咱們能夠任意修改對象內的屬性:
var person = { name: 'semlinker', age: 31 }; person.name = 'fer'; console.log(person.name); // Ouput: 'fer'
上面代碼中咱們先建立一個 person 對象,而後修改 person 對象的 name 屬性,最終輸出修改後的 name 屬性。接下來咱們調整一下上面的代碼,調整後的代碼以下:
var person = { name: 'semlinker', age: 31 }; var aliasPerson = person; person.name = 'fer'; console.log(aliasPerson === person); // Output: true
在修改 person 對象前,咱們先把 person 對象賦值給 aliasPerson 變量,在修改完 person 對象的屬性以後,咱們使用 ===
比較 aliasPerson 與 person,發現輸出的結果是 true。也許你已經知道了,咱們剛纔在 AppComponent 中模型更新了,但視圖卻未同步更新的緣由。
接下來咱們來介紹一下 Immutable
:
Immutable 即不可變,表示當數據模型發生變化的時候,咱們不會修改原有的數據模型,而是建立一個新的數據模型。具體示例以下:
var person = { name: 'semlinker', age: 31 }; var newPerson = Object.assign({}, person, {name: 'fer'}); console.log(person.name, newPerson.name); // Output: 'semliker' 'fer' console.log(newPerson === person); // Output: false
此次要修改 person 對象中的 name 屬性,咱們不是直接修改原有對象,而是使用 Object.assign 方法建立一個新的對象。介紹完 Mutable 和 Immutable 的概念 ,咱們回過頭來分析一下 OnPush 策略,該策略內部使用 looseIdentical 函數來進行對象的比較,looseIdentical 的實現以下:
export function looseIdentical(a: any, b: any): boolean { return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b); }
所以當咱們使用 OnPush 策略時,須要使用的 Immutable 的數據結構,才能保證程序正常運行。爲了提升變化檢測的性能,咱們應該儘量在組件中使用 OnPush 策略,爲此咱們組件中所需的數據,應僅依賴於輸入屬性。
OnPush 策略是提升應用程序性能的一個簡單而好用的方法。不過,咱們還有其餘方法來得到更好的性能。 即便用 Observable 與 ChangeDetectorRef 對象提供的 API,來手動控制組件的變化檢測行爲。
ChangeDetectorRef 是組件的變化檢測器的引用,咱們能夠在組件中的經過依賴注入的方式來獲取該對象:
import { ChangeDetectorRef } from '@angular/core'; @Component({}) class MyComponent { constructor(private cdRef: 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() - 從該組件到各個子組件執行一次變化檢測
接下來咱們先來看一下 markForCheck()
方法的使用示例:
child.component.ts
import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'exe-child', template: ` <p>當前值: {{ counter }}</p> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ChildComponent implements OnInit { @Input() counter: number = 0; constructor(private cdRef: ChangeDetectorRef) {} ngOnInit() { setInterval(() => { this.counter++; this.cdRef.markForCheck(); }, 1000); } }
parent.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'exe-parent', template: ` <exe-child></exe-child> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ParentComponent { }
ChildComponent 組件設置的變化檢測策略是 OnPush 策略,此外該組件也沒有任何輸入屬性。那麼咱們應該怎麼執行變化檢測呢 ?咱們看到在 ngOnInit 鉤子中,咱們經過 setInterval 定時器,每隔一秒鐘更新計數值同時調用 ChangeDetectorRef 對象上的 markForCheck() 方法,來標識該組件在下一個變化檢測週期,需執行變化檢測,從而更新視圖。
接下來咱們來說一下 detach() 和 reattach() 方法,它們用來開啓/關閉組件的變化檢測。讓咱們看下面的例子:
child.component.ts
import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'exe-child', template: ` Detach: <input type="checkbox" (change)="detachCD($event.target.checked)"> <p>當前值: {{ counter }}</p> ` }) export class ChildComponent implements OnInit { counter: number = 0; constructor(private cdRef: ChangeDetectorRef) { } ngOnInit() { setInterval(() => { this.counter++; }, 1000); } detachCD(checked: boolean) { if (checked) { this.cdRef.detach(); } else { this.cdRef.reattach(); } } }
該組件有一個用於移除或添加變化檢測器的複選框。 當複選框被選中時,detach() 方法將被調用,以後組件及其子組件將不會被檢查。當取消選擇時,reattach() 方法會被調用,該組件將會被從新添加到變化檢測器樹上。
使用 Observables 機制提高性能和不可變的對象相似,但當發生變化的時候,Observables 不會建立新的模型,但咱們能夠經過訂閱 Observables 對象,在變化發生以後,進行視圖更新。使用 Observables 機制的時候,咱們一樣須要設置組件的變化檢測策略爲 OnPush。咱們立刻看個例子:
counter.component.ts
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(); }); } }
app.component.ts
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Rx'; @Component({ selector: 'exe-app', template: ` <exe-counter [addStream]='counterStream'></exe-counter> ` }) export class AppComponent implements OnInit { counterStream: Observable<any>; ngOnInit() { this.counterStream = Observable.timer(0, 1000); } }
如今咱們來總結一下變化檢測的原理:Angular 應用是一個響應系統,變化檢測老是從根組件到子組件這樣一個從上到下的順序開始執行,它是一棵線性的有向樹,默認狀況下,變化檢測系統將會走遍整棵樹,但咱們可使用 OnPush 變化檢測策略,在結合 Observables 對象,進而利用 ChangeDetectorRef 實例提供的方法,來實現局部的變化檢測,最終提升系統的總體性能。
1.ChangeDetectionStrategy 變化檢測策略總共有幾種 ?
export declare enum ChangeDetectionStrategy { OnPush = 0, // 變化檢測器的狀態值是 CheckOnce Default = 1, // 組件默認值 - 變化檢測器的狀態值是 CheckAlways,即始終執行變化檢測 }
2.變化檢測器的狀態有哪幾種 ?
export declare enum ChangeDetectorStatus { CheckOnce = 0, // 表示在執行detectChanges以後,變化檢測器的狀態將會變成Checked Checked = 1, // 表示變化檢測將被跳過,直到變化檢測器的狀態恢復成CheckOnce CheckAlways = 2, // 表示在執行detectChanges以後,變化檢測器的狀態始終爲CheckAlways Detached = 3, // 表示該變化檢測器樹已從根變化檢測器樹中移除,變化檢測將會被跳過 Errored = 4, // 表示在執行變化檢測時出現異常 Destroyed = 5, // 表示變化檢測器已被銷燬 }
3.markForCheck()、detectChanges()、detach()、reattach() (@angular/core version: 2.2.4)
markForCheck()
ViewRef_.prototype.markForCheck = function () { this._view.markPathToRootAsCheckOnce(); }; AppView.prototype.markPathToRootAsCheckOnce = function () { var c = this; while (isPresent(c) && c.cdMode !== ChangeDetectorStatus.Detached) { if (c.cdMode === ChangeDetectorStatus.Checked) { c.cdMode = ChangeDetectorStatus.CheckOnce; } if (c.type === ViewType.COMPONENT) { c = c.parentView; } else { c = c.viewContainer ? c.viewContainer.parentView : null; } } };
detectChanges()
ViewRef_.prototype.detectChanges = function () { this._view.detectChanges(false); triggerQueuedAnimations(); }; AppView.prototype.detectChanges = function (throwOnChange) { var s = _scope_check(this.clazz); if (this.cdMode === ChangeDetectorStatus.Checked || this.cdMode === ChangeDetectorStatus.Errored || this.cdMode === ChangeDetectorStatus.Detached) return; if (this.cdMode === ChangeDetectorStatus.Destroyed) { this.throwDestroyedError('detectChanges'); } this.detectChangesInternal(throwOnChange); if (this.cdMode === ChangeDetectorStatus.CheckOnce) this.cdMode = ChangeDetectorStatus.Checked; this.numberOfChecks++; wtfLeave(s); };
detach()
ViewRef_.prototype.detach = function () { this._view.cdMode = ChangeDetectorStatus.Detached; };
reattach()
ViewRef_.prototype.reattach = function () { this._view.cdMode = this._originalMode; this.markForCheck(); };
經過兩篇文章,咱們詳細介紹了 Angular 2 變化檢測的內容,此外在瞭解變化檢測的基礎上,咱們還介紹瞭如何基於 OnPush 變化檢測策略,進行變化檢測的優化,從而提升應用程序的性能。