詳解angular2組件中的變化檢測機制(對比angular1的臟檢測)


組件和變化檢測器

如你所知,Angular 2 應用程序是一顆組件樹,而每個組件都有自己的變化檢測器,這意味着應用程序也是一顆變化檢測器樹。順便說一句,你可能會想。是由誰來生成變化檢測器?這是個好問題,它們是由代碼生成。 Angular 2 編譯器為每個組件自動創建變化檢測器,而且最終生成的這些代碼 JavaScript VM友好代碼。這也是為什么新的變化檢測是快速的 (相比於 Angular 1.x 的 $digest)。基本上,每個組件可以在幾毫秒內執行數萬次檢測。因此你的應用程序可以快速執行,而無需調整性能。

另外在 Angular 2 中,任何數據都是從頂部往底部流動,即單向數據流。下圖是 Angular 1.x 與 Angular 2 變化檢測的對比圖:

讓我們來看一下具體例子:

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.ts

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' 。這雖然很簡單,但很重要。另外對於單次變化檢測,每個組件只檢查一次。

OnChanges

當組件的任何輸入屬性發生變化的時候,我們可以通過組件生命周期提供的鈎子 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 的檢測策略。 

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 的概念。

Mutable(可變) and 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

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 對象,在變化發生之后,進行視圖更新。使用 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, // 表示變化檢測器已被銷毀 }


原文地址:http://www.tuicool.com/articles/UBBzQzA


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM