原文:The difference between NgDoCheck and AsyncPipe in OnPush components
做者:Max Koretskyi
原技術博文由Max Koretskyi
撰寫發佈,他目前於 ag-Grid 擔任開發大使(Developer Advocate)
譯者按:開發大使負責確保其所在的公司認真聽取社區的聲音並向社區傳達他們的行動及目標,其做爲社區和公司之間的紐帶存在。
譯者:Ice Panpan;校對者:vaanxygit
這篇文章是對Shai這條推特的迴應。他詢問使用 NgDoCheck
生命週期鉤子來手動比較值而不是使用 asyncPipe
是否有意義。這是一個很是好的問題,須要對引擎的工做原理有不少了解:變化檢測(change detection),管道(pipe)和生命週期鉤子(lifecycle hooks)。那就是我探索的入口😎。github
在本文中,我將向您展現如何手動處理變動檢測。這些技術使您能夠更好地掌控 Angular 的輸入綁定(input bindings)的自動執行和異步值檢查(async values checks)。掌握了這些知識以後,我還將與您分享我對這些解決方案的性能影響的見解。讓咱們開始吧!api
在 Angular 中,咱們有一種很是常見的優化技術,須要將 ChangeDetectionStrategy.OnPush
添加到組件中。假設咱們有以下兩個簡單的組件:app
@Component({
selector: 'a-comp',
template: ` <span>I am A component</span> <b-comp></b-comp> `
})
export class AComponent {}
@Component({
selector: 'b-comp',
template: `<span>I am B component</span>`
})
export class BComponent {}
複製代碼
這樣設置以後, Angular 每次都會對 A
和 B
兩個組件運行變動檢測。若是咱們如今爲 B
組件添加上 OnPush
策略:dom
@Component({
selector: 'b-comp',
template: `<span>I am B component</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {}
複製代碼
只有在輸入綁定的值發生變化時 Angular 纔會對 B
運行變動檢測。因爲它如今沒有任何綁定,所以該組件只會在初始化的時候檢查一次。異步
有沒有辦法強制對 B
組件進行變動檢測?是的,咱們能夠注入 changeDetectorRef
並使用它的方法 markForCheck
來指示 Angular 須要檢查該組件。而且因爲 NgDoCheck 鉤子仍然會被 B 組件觸發,因此咱們應該在 NgDoCheck 中調用 markForCheck:async
@Component({
selector: 'b-comp',
template: `<span>I am B component</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
constructor(private cd: ChangeDetectorRef) {}
ngDoCheck() {
this.cd.markForCheck();
}
}
複製代碼
如今,當 Angular 檢查父組件 A
時,將始終檢查 B
組件。如今讓咱們看看咱們能夠在哪裏使用它。函數
我以前說過,Angular 只在 OnPush
組件的綁定發生變化時運行的變化檢測。因此讓咱們看一下輸入綁定的例子。假設咱們有一個經過輸入綁定從父組件傳遞下來的對象:工具
@Component({
selector: 'b-comp',
template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
@Input() user;
}
複製代碼
在父組件 A
中,咱們定義了一個對象,並實現了在單擊按鈕時來更新對象名稱的 changeName 方法:性能
@Component({
selector: 'a-comp',
template: ` <span>I am A component</span> <button (click)="changeName()">Trigger change detection</button> <b-comp [user]="user"></b-comp> `
})
export class AComponent {
user = {name: 'A'};
changeName() {
this.user.name = 'B';
}
}
複製代碼
若是您如今運行此示例,則在第一次變動檢測後,您將看到用戶名稱被打印出來:
User name: A
複製代碼
可是當咱們點擊按鈕並回調中更更名稱時:
changeName() {
this.user.name = 'B';
}
複製代碼
該名稱並無在屏幕上更新,這是由於 Angular 對輸入參數執行淺比較,而且對 user 對象的引用沒有改變。那咱們怎麼解決這個問題呢?
好吧,咱們能夠在檢測到差別時手動檢查名稱並觸發變動檢測:
@Component({
selector: 'b-comp',
template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
@Input() user;
previousName = '';
constructor(private cd: ChangeDetectorRef) {}
ngDoCheck() {
if (this.previousName !== this.user.name) {
this.previousName = this.user.name;
this.cd.markForCheck();
}
}
}
複製代碼
若是您如今運行此代碼,你將在屏幕上看到更新的名稱。
如今,讓咱們的例子更復雜一點。咱們將介紹一種基於 RxJs 的服務,它能夠異步發出更新。這相似於 NgRx 的體系結構。我將使用一個 BehaviorSubject
做爲值的來源,由於咱們須要在這個流的最開始設置初始值:
@Component({
selector: 'a-comp',
template: ` <span>I am A component</span> <button (click)="changeName()">Trigger change detection</button> <b-comp [user]="user"></b-comp> `
})
export class AComponent {
stream = new BehaviorSubject({name: 'A'});
user = this.stream.asObservable();
changeName() {
this.stream.next({name: 'B'});
}
}
複製代碼
因此咱們須要在子組件中訂閱這個流並從中獲取到 user
對象。咱們須要訂閱流並檢查值是否更新。這樣作的經常使用方法是使用 AsyncPipe。
因此這裏是子組件 B
的實現:
@Component({
selector: 'b-comp',
template: ` <span>I am B component</span> <span>User name: {{(user | async).name}}</span> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
@Input() user;
}
複製代碼
這是演示。可是,還有另外一種不使用管道的方法嗎?
是的,咱們能夠手動檢查值並在須要時觸發變動檢測。正如開頭的例子同樣,咱們可使用 NgDoCheck
生命週期鉤子:
@Component({
selector: 'b-comp',
template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
@Input('user') user$;
user;
previousName = '';
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.user$.subscribe((user) => {
this.user = user;
})
}
ngDoCheck() {
if (this.previousName !== this.user.name) {
this.previousName = this.user.name;
this.cd.markForCheck();
}
}
}
複製代碼
你能夠在這查看。
咱們但願把值的比較與更新邏輯從 NgDoCheck
中移至訂閱的回調函數,由於咱們是從那裏獲取到新值的:
export class BComponent {
@Input('user') user$;
user = {name: null};
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.user$.subscribe((user) => {
if (this.user.name !== user.name) {
this.cd.markForCheck();
this.user = user;
}
})
}
}
複製代碼
例子在這。
有趣的是,這其實正是 AsyncPipe 背後的工做原理:
@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
constructor(private _ref: ChangeDetectorRef) {}
transform(obj: ...): any {
...
this._subscribe(obj);
...
if (this._latestValue === this._latestReturnedValue) {
return this._latestReturnedValue;
}
this._latestReturnedValue = this._latestValue;
return WrappedValue.wrap(this._latestValue);
}
private _subscribe(obj): void {
...
this._strategy.createSubscription(
obj, (value: Object) => this._updateLatestValue(obj, value));
}
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
}
複製代碼
如今咱們知道如何使用手動進行變動檢測而不是使用 AsyncPipe,讓咱們回答下最一開始的問題。那種方法更快?
嗯...這取決於你如何比較它們,但在其餘條件相同的狀況下,手動方法會更快。儘管我不認爲二者會有明顯區別。如下是爲何手動方法能夠更快的幾個例子。
就內存而言,您不須要建立 Pipe 類的實例。就編譯時間而言,編譯器沒必要花時間解析管道特定語法並生成管道特定輸出。就運行時間而言,節省了異步管道爲組件進行變動檢測所調用的函數的時間。這個例子演示了當代碼中包含 pipe 時 updateRenderer 所生成的代碼:
function (_ck, _v) {
var _co = _v.component;
var currVal_0 = jit_unwrapValue_7(_v, 3, 0, asyncpipe.transform(_co.user)).name;
_ck(_v, 3, 0, currVal_0);
}
複製代碼
如您所見,異步管道的代碼調用管道實例上的 transform
方法以獲取新值。管道將返回從訂閱中收到的最新值。
將其與爲手動方法生成的普通代碼進行比較:
function(_ck,_v) {
var _co = _v.component;
var currVal_0 = _co.user.name;
_ck(_v,3,0,currVal_0);
}
複製代碼
這就是 Angular 在檢查 B
組件時調用的方法。
與執行淺比較的輸入綁定不一樣,異步管道的實現根本不執行比較(感謝 Olena Horal 注意到這一點)。它將每一個新發射的值認爲是更新,即便它與先前發射的值同樣。下面的代碼是父組件 A
的實現,它每次都發射出相同的對象。儘管如此,Angular 仍然會對 B
組件進行變動檢測:
export class AComponent {
o = {name: 'A'};
user = new BehaviorSubject(this.o);
changeName() {
this.user.next(this.o);
}
}
複製代碼
這意味着每次發出新值時,使用異步管道的組件都會被標記以進行檢查。而且 Angular 將在下次運行變動檢測時檢查該組件,即便該值未更改。
這是應用於什麼狀況呢?嗯...在咱們的例子中,咱們只關注 user
對象的 name
屬性,由於咱們須要在模板中使用它。咱們並不關心整個對象以及對象的引用可能會改變的事實。若是 name 沒有發生改變,咱們不須要從新渲染組件。但你沒法用異步管道來避免這種狀況。
NgDoCheck
並非沒有問題:)因爲僅在檢查父組件時觸發鉤子,若是其中一個父組件使用 OnPush
策略而且在變動檢測期間未檢查,則不會觸發該鉤子。所以,當您經過服務收到新值時,不能依賴它來觸發變動檢測。在這種狀況下,我在訂閱回調中調用 markForCheck
方法是正確的解決方案。
基本上,手動比較可讓您更好地控制檢查。您能夠定義什麼時候須要檢查組件。這與許多其餘工具相同 - 手動控制爲您提供了更大的靈活性,但您必須知道本身在作什麼。爲了得到這些知識,我鼓勵您投入時間和精力學習和閱讀更多文章。
你不用擔憂 NgDoCheck
生命週期鉤子被調用的頻率,或者它會比管道的 transform
方法更頻繁地被調用。首先,我上面已經展現瞭解決方案,當使用異步流時,你應該在訂閱的回調中而非在該鉤子函數中手動執行變動檢測。其次,只有在父組件被檢測後纔會調用該鉤子函數。若是父組件沒有被檢查,則不會調用該鉤子。對於管道而言,因爲流中的淺比較和更改引用的緣由,管道的 transform
方法被調用的次數只會和手動方法相同甚至更多。
從這5篇文章入手會讓你成爲Angular Change Detection 的專家。若是你想要牢固掌握 Angular 中變動檢測機制,那麼這一系列的文章是必讀的。每一篇文章都會基於前一篇文章中所解釋的相關信息,既包含高層次的概述又囊括了具體的實現細節,而且都附有相關源代碼。