原文连接: 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均未更改,所以不会去检查其视图。
而且,经过建立更小粒度的组件,咱们的代码变得更具可读性和可重用性。