原文:
https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4netbasal.comAngular 默认采用 ChangeDetectionStrategy.Default
作为变更检测策略。
默认的策略不需要对应用程序做任何假设,因此每当我们的应用程序发生变化时,由于各种用户事件、计时器、XHR、promise等的原因,所有组件都会运行一个变化检测。
这意味着,从单击事件到从ajax调用,都会触发更改检测。
我们可以很容易地通过在组件中创建一个getter,并在模板中使用它,来观察变更检测机制的调用。例如
@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 都会运行一个变更检测周期,我们应该会在控制台中看到两个检查视图的 log 。
这种技术称为脏检查。为了知道视图是否应该更新,Angular需要访问新值,将其与旧值进行比较,并决定是否更新视图。
现在,想象一个包含数千个表达式的大型应用程序;如果我们让Angular在变更检测周期运行时检查它们中的每一个,我们可能会遇到性能问题。
尽管Angular非常快,但随着应用程序的增长,Angular跟踪所有这些变化将会更加吃力。
如果我们能告诉Angular在一个合适的时机去更新组件,那么将会极大优化应用的性能。
OnPush Change Detection Strategy
我们可以将组件的ChangeDetectionStrategy
设置为ChangeDetectionStrategy.OnPush
这告诉Angular,组件只依赖于它的@input()
,只需要在以下情况下进行检查
1.Input
即组件的输入的值的引用改变
通过设置onPush变更检测策略,我们与Angular约定,我们将使用immutable
不可变对象(或可观察对象,我们将在后面看到)。
在变更检测中使用immutable
的好处是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';
}
}
当我们点击按钮,我们将不会看到任何 log 。这是因为Angular是通过引用来比较旧值和新值的,就像这样
/** Returns false in our case */
if( oldValue !== newValue ) {
runChangeDetection();
}
再次提示下,数字、布尔值、字符串、null和undefined的都是基本类型。所有基本类型都通过值传递。对象、数组和函数也通过值传递,但该值是引用的副本。
因此,为了在组件中触发更改检测,我们需要更改对象引用。
@Component({
template: `
<tooltip [config]="config"></tooltip>
`
})
export class AppComponent {
config = {
position: 'top'
};
onClick() {
this.config = {
position: 'bottom'
}
}
}
通过这个更改,我们将看到视图已被选中,新值将按预期显示。
2. 组件内部或者子组件触发事件
组件可以有一个内部状态,当从组件或其子组件之一触发事件时将触发一次变更检测以更新视图
@Component({
template: `
<button (click)="add()">Add</button>
{{count}}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
add() {
this.count++;
}
}
您可能会想,这应该适用于每个触发变更检测的异步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++;
}
}
也就是说当你设置 OnPush 模式时,如果你需要更新非@input
申明的状态,必须依赖于dom
事件,其他的异步事件不会触发变更检测。
3.手动触发变更检测
Angular为我们提供了三种方法来在需要的时候触发变更检测。
第一个是 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
让我们来看一个带有 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 管道触发变更检测
@Component({
template: `
<div *ngFor="let item of items | async">{{item.title}}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
@Input() items;
}
现在我们可以看到,当我们单击按钮时,视图被更新了。这样做的原因是,当发出一个新值时,异步管道会将组件标记为要检查更改。我们可以在源代码中看到它
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
Angular为我们调用了markForCheck(),这就是为什么即使引用没有改变,视图也会更新的原因。
一些疑问
@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>
也许你的期望是三秒钟后Angular会用新内容更新标签组件视图。
毕竟,我们看到如果更新onPush组件中的输入引用,应该会触发更改检测,难道不是吗?
不幸的是,在这种情况下,它不是这样工作的。Angular无法知道我们正在更新标签组件中的属性。在模板中定义input()是让Angular知道应该在变更检测周期中检查该属性的唯一方法。
eg:
<app-tabs>
<app-tab [content]="content"></app-tab>
</app-tabs>
因为我们在模板中显式地定义了input(), Angular创建了一个名为updateRenderer()的函数,它在每个变更检测周期中跟踪内容值。
在这些情况下,简单的解决方案是使用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的力量之后,我们可以利用它来创建一个更高性能的应用程序。onPush组件越多,Angular需要执行的检查就越少。让我们来看一个真实的例子
假设我们有一个todos组件,它将todos作为输入
@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' }];
}
}
上述方法的缺点是,当我们点击add按钮时,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;
}
}
现在,当我们单击add按钮时,我们将在控制台中看到一个日志,因为其他todo组件的输入都没有改变,因此它们的视图没有被检查。
此外,通过创建独立组件,我们可以使代码更具可读性和可重用性。