原文連接: A Comprehensive Guide to Angular onPush Change Detection Strategyjavascript
原文做者:Netanel Basalhtml
譯者:淼淼java
默認狀況下,Angular使用ChangeDetectionStrategy.Default
策略來進行變動檢測。ajax
默認策略並不事先對應用作出任何假設,所以,每當用戶事件、記時器、XHR、promise等事件使應用中的數據將發生了改變時,全部的組件中都會執行變動檢測。promise
這意味着從點擊事件到從ajax調用接收到的數據之類的任何事件都會觸發更改檢測。bash
經過在組件中定義一個getter而且在模板中使用它,咱們能夠很容易的看出這一點:app
@Component({
template: ` <h1>Hello {{name}}!</h1> {{runChangeDetection}} `
})
export class HelloComponent {
@Input() name: string;
get runChangeDetection() {
console.log('Checking the view');
return true;
}
}
複製代碼
@Component({
template: ` <hello></hello> <button (click)="onClick()">Trigger change detection</button> `
})
export class AppComponent {
onClick() {}
}
複製代碼
執行以上代碼後,每當咱們點擊按鈕時。Angular將會執行一遍變動檢測循環,在console裏咱們能夠看到兩行「Checking the view」的日誌。dom
這種技術被稱做髒檢查。爲了知道視圖是否須要更新,Angular須要訪問新值並和舊值比較來判斷是否須要更新視圖。異步
如今想象一下,若是有一個有成千上萬個表達式的大應用,Angular去檢查每個表達式,咱們可能會遇到性能上的問題。async
那麼有沒有辦法讓咱們主動告訴Angular何時去檢查咱們的組件呢?
咱們能夠將組件的ChangeDetectionStrategy
設置成ChangeDetectionStrategy.OnPush
。
這將告訴Angular該組件僅僅依賴於它的@inputs()
,只有在如下幾種狀況才須要檢查:
Input
引用發生改變經過設置onPush
變動檢測測策略,咱們與Angular約定強制使用不可變對象(或稍後將要介紹的observables)。
在變動檢測的上下文中使用不可變對象的好處是,Angular能夠經過檢查引用是否發生了改變來判斷視圖是否須要檢查。這將會比深度檢查要容易不少。
讓咱們試試來修改一個對象而後看看結果。
@Component({
selector: 'tooltip',
template: ` <h1>{{config.position}}</h1> {{runChangeDetection}} `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent {
@Input() config;
get runChangeDetection() {
console.log('Checking the view');
return true;
}
}
複製代碼
@Component({
template: `
<tooltip [config]="config"></tooltip>
`
})
export class AppComponent {
config = {
position: 'top'
};
onClick() {
this.config.position = 'bottom';
}
}
複製代碼
這時候去點擊按鈕時看不到任何日誌了,這是由於Angular將舊值和新值的引用進行比較,相似於:
/** Returns false in our case */
if( oldValue !== newValue ) {
runChangeDetection();
}
複製代碼
值得一提的是numbers, booleans, strings, null 、undefined都是原始類型。全部的原始類型都是按值傳遞的. Objects, arrays, 還有 functions 也是按值傳遞的,只不過值是引用地址的副本。
因此爲了觸發對該組件的變動檢測,咱們須要更改這個object的引用。
@Component({
template: ` <tooltip [config]="config"></tooltip> `
})
export class AppComponent {
config = {
position: 'top'
};
onClick() {
this.config = {
position: 'bottom'
}
}
}
複製代碼
將對象引用改變後,咱們將看到視圖已被檢查,新值被展現出來。
當在一個組件或者其子組件中觸發了某一個事件時,這個組件的內部狀態會更新。 例如:
@Component({
template: ` <button (click)="add()">Add</button> {{count}} `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
add() {
this.count++;
}
}
複製代碼
當咱們點擊按鈕時,Angular執行變動檢測循環並更新視圖。
你可能會想,按照咱們開頭講述的那樣,每一次異步的API都會觸發變動檢測,可是並非這樣。
你會發現這個規則只適用於DOM事件,下面這些API並不會觸發變動檢測:
@Component({
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
constructor() {
setTimeout(() => this.count = 5, 0);
setInterval(() => this.count = 5, 100);
Promise.resolve().then(() => this.count = 5);
this.http.get('https://count.com').subscribe(res => {
this.count = res;
});
}
add() {
this.count++;
}
複製代碼
注意你仍然是更新了該屬性的,因此在下一個變動檢測流程中,好比去點擊按鈕,count值將會變成6(5+1)。
Angular給咱們提供了3種方法來觸發變動檢測。
第一個是detectChanges()
來告訴Angular在該組件和它的子組件中去執行變動檢測。
@Component({
selector: 'counter',
template: `{{count}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
constructor(private cdr: ChangeDetectorRef) {
setTimeout(() => {
this.count = 5;
this.cdr.detectChanges();
}, 1000);
}
}
複製代碼
第二個是ApplicationRef.tick()
,它告訴Angular來對整個應用程序執行變動檢測。
tick() {
try {
this._views.forEach((view) => view.detectChanges());
...
} catch (e) {
...
}
}
複製代碼
第三是markForCheck()
,它不會觸發變動檢測。相反,它會將全部設置了onPush的祖先標記,在當前或者下一次變動檢測循環中檢測。
markForCheck(): void {
markParentViewsForCheck(this._view);
}
export function markParentViewsForCheck(view: ViewData) {
let currView: ViewData|null = view;
while (currView) {
if (currView.def.flags & ViewFlags.OnPush) {
currView.state |= ViewState.ChecksEnabled;
}
currView = currView.viewContainerParent || currView.parent;
}
}
複製代碼
須要注意的是,手動執行變動檢測並非一種「hack」,這是Angular有意的設計而且是很是合理的行爲(固然,是在合理的場景下)。
async
pipe會訂閱一個 Observable 或 Promise,並返回它發出的最近一個值。
讓咱們看一個input()
是observable的onPush組件。
@Component({
template: ` <button (click)="add()">Add</button> <app-list [items$]="items$"></app-list> `
})
export class AppComponent {
items = [];
items$ = new BehaviorSubject(this.items);
add() {
this.items.push({ title: Math.random() })
this.items$.next(this.items);
}
}
複製代碼
@Component({
template: ` <div *ngFor="let item of _items ; ">{{item.title}}</div> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
@Input() items: Observable<Item>;
_items: Item[];
ngOnInit() {
this.items.subscribe(items => {
this._items = items;
});
}
}
複製代碼
當咱們點擊按鈕並不能看到視圖更新。這是由於上述提到的幾種狀況均未發生,因此Angular在當前變動檢測循環並不會檢車該組件。
如今,讓咱們加上async
pipe試試。
@Component({
template: ` <div *ngFor="let item of items | async">{{item.title}}</div> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
@Input() items;
}
複製代碼
如今能夠看到當咱們點擊按鈕時,視圖也更新了。緣由是當新的值被髮射出來時,async
pipe將該組件標記爲發生了更改須要檢查。咱們能夠在源碼中看到:
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
複製代碼
Angular爲咱們調用markForCheck()
,因此咱們能看到視圖更新了即便input的引用沒有發生改變。
若是一個組件僅僅依賴於它的input屬性,而且input屬性是observable,那麼這個組件只有在它的input屬性發射一個事件的時候纔會發生改變。
Quick tip:對外部暴露你的subject是不值得提倡的,老是使用asObservable()
方法來暴露該observable。
@Component({
selector: 'app-tabs',
template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
@ContentChild(TabComponent) tab: TabComponent;
ngAfterContentInit() {
setTimeout(() => {
this.tab.content = 'Content';
}, 3000);
}
}
複製代碼
@Component({
selector: 'app-tab',
template: `{{content}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
@Input() content;
}
複製代碼
<app-tabs>
<app-tab></app-tab>
</app-tabs>
複製代碼
也許你會覺得3秒後Angular將會使用新的內容更新tab組件。
畢竟,咱們更新來onPush組件的input引用,這將會觸發變動檢測不是嗎?
然而,在這種狀況下,它並不生效。Angular不知道咱們正在更新tab組件的input屬性,在模板中定義input()
是讓Angular知道應在變動檢測循環中檢查此屬性的惟一途徑。
例如:
<app-tabs>
<app-tab [content]="content"></app-tab>
</app-tabs>
複製代碼
由於當咱們明確的在模板中定義了input()
,Angular會建立一個叫updateRenderer()
的方法,它會在每一個變動檢測循環中都對content的值進行追蹤。
在這種狀況下簡單的解決辦法使用setter而後調用markForCheck()
。
@Component({
selector: 'app-tab',
template: ` {{_content}} `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
_content;
@Input() set content(value) {
this._content = value;
this.cdr.markForCheck();
}
constructor(private cdr: ChangeDetectorRef) {}
}
複製代碼
在理解了onPush
的強大以後,咱們來利用它創造一個更高性能的應用。onPush組件越多,Angular須要執行的檢查就越少。讓咱們看看你一個真是的例子:
咱們又一個todos
組件,它有一個todos做爲input()。
@Component({
selector: 'app-todos',
template: ` <div *ngFor="let todo of todos"> {{todo.title}} - {{runChangeDetection}} </div> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
@Input() todos;
get runChangeDetection() {
console.log('TodosComponent - Checking the view');
return true;
}
}
複製代碼
@Component({
template: ` <button (click)="add()">Add</button> <app-todos [todos]="todos"></app-todos> `
})
export class AppComponent {
todos = [{ title: 'One' }, { title: 'Two' }];
add() {
this.todos = [...this.todos, { title: 'Three' }];
}
}
複製代碼
上述方法的缺點是,當咱們單擊添加按鈕時,即便以前的數據沒有任何更改,Angular也須要檢查每一個todo。所以第一次單擊後,控制檯中將顯示三個日誌。
在上面的示例中,只有一個表達式須要檢查,可是想象一下若是是一個有多個綁定(ngIf,ngClass,表達式等)的真實組件,這將會很是耗性能。
咱們白白的執行了變動檢測!
更高效的方法是建立一個todo組件並將其變動檢測策略定義爲onPush。例如:
@Component({
selector: 'app-todos',
template: ` <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
@Input() todos;
}
@Component({
selector: 'app-todo',
template: `{{todo.title}} {{runChangeDetection}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
@Input() todo;
get runChangeDetection() {
console.log('TodoComponent - Checking the view');
return true;
}
}
複製代碼
如今,當咱們單擊添加按鈕時,控制檯中只會看到一個日誌,由於其餘的todo組件的input均未更改,所以不會去檢查其視圖。
而且,經過建立更小粒度的組件,咱們的代碼變得更具可讀性和可重用性。