事情是这样的,今天在看NGZORRO时候看见这么一句话。
OnPush模式,性能卓越。🤔 那我就来看看ant desgin是怎么做出性能优化的。
Angular有两种变化检测策略(Change Detection Strategy)
enum ChangeDetectionStrategy {
// 使用 CheckOnce 策略,这意味着自动更改检测将停用,直到通过将策略设置为"Default"(总是检查)重新激活。
OnPush: 0
// 使用默认的 CheckAlway 策略,其中更改检测是自动的,直到显式停用
Default: 1
}
在一般情况下,Angular组件的默认变化检测策略默认为Default。
import { Component, OnInit } from "@angular/core";
@Component({
selector: "app-timer",
template: `
<div>Counter-timer: {{ timer }}</div>
`
})
export class TimerComponent implements OnInit {
timer: number = 0;
ngOnInit() {
setInterval(() => {
this.timer++;
console.log(this.timer);
}, 1000);
}
}
父组件引用计时组件TimerComponent
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<app-timer></app-timer>
`
})
export class AppComponent {}
可以看到页面上的计时在不断的跳动,这个timer组件的计时数据完全由自身的setInterval触发,而Angular的变化检测会不停的从父组件开始检测,并向下检测子组件的数据变化,一但检测到有数据变化,立即渲染页面。这种默认的变化检测策略使用起来很方便,无需多余的代码,但如果组件众多,组件中又包含组件,那每次这种不加区分的变化检测对页面的性能却有一定的消耗,而针对这种问题优化就有了onPush的变化检测策略。
使用变化检测策略OnPush是Angular常见的页面性能优化手段。
@Component({
// ...
changeDetection: ChangeDetectionStrategy.OnPush //设置变化检测策略为OnPush
}
export class TimerComponent {
// ...
}
当组件使用OnPush变化检测策略时,只有在以下任一情况下,Angular才会检测此组件
1.组件的任一@Input属性被父组件设为新值,如果@Input是引用类型,则需被设为新的实例才会触发变化检测。
2.事件发生。
当TimerComponent使用OnPush变化检测策略后,再看AppComponent页面时,计时器不再跳动,始终是0,但在控制台中,你可以看到timer的值每秒其实是在变化的,只是页面没有渲染而已。
这时如果我们给TimerComponent组件加上一个无关紧要的输入参数x,这个x不参与任何计时逻辑,代码如下
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit
} from "@angular/core";
@Component({
selector: "app-timer",
template: `
<div>Counter-timer: {{ timer }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
timer: number = 0;
@Input() x: number = 0; // 输入参数x为数值型,并且不参与任何业务
ngOnInit() {
setInterval(() => {
this.timer++;
console.log(this.timer);
}, 1000);
}
}
我们在AppComponent中添加一个按钮用来修改i值,并将i传入到子组件TimerComponent中,代码如下
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<app-timer [x]="i"></app-timer>
<button (click)="addi()">Add i</button>
`
})
export class AppComponent {
i: number = 0;
addi() {
this.i++;
}
}
运行代码,查看AppComponent页面,你可以发现页面上计时器不跳动,但当你点击Add i按钮后,计时器就会刷新数字。这是因为当子组件使用OnPush变化检测策略时,子组件的任一@Input属性被父组件设为新值时,无论这个@Input属性是否影响到当前页面,Angular都会对此组件进行一次变化检测。
我们再试一下@Input属性传入的是引用型变量,计时子组件代码如下
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit
} from "@angular/core";
import { DataModel } from "../app.component";
@Component({
selector: "app-timer",
template: `
<div>Counter-timer: {{ timer }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
timer: number = 0;
@Input() x: DataModel = new DataModel(); // 输入参数改为引用型
ngOnInit() {
setInterval(() => {
this.timer++;
console.log(this.timer);
}, 1000);
}
}
AppComponent父组件代码如下
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<app-timer [x]="data"></app-timer>
<button (click)="addi()">Add i</button>
`
})
export class AppComponent {
data: DataModel = new DataModel();
addi() {
this.data.i++;
}
}
export class DataModel {
i: number = 0;
}
运行代码后,你可以发现页面上计时器不跳动,但当你点击Add i按钮后,计时器仍旧不跳动。这是因为只改变引用类型里的某个值,Angular认为@Input传入值还是同样的引用地址,算没有设为新值,从而没有触发变化检测。如需触发检测,则需要传入不同的引用地址,也就是要传一个全新的引用型变量,此例,只要将AppComponent父组件代码增加一句话就行
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<app-timer [x]="data"></app-timer>
<button (click)="addi()">Add i</button>
`
})
export class AppComponent {
data: DataModel = new DataModel();
addi() {
this.data.i++;
this.data = { i: this.data.i }; // 新增的语句,将data变成新的实例
}
}
export class DataModel {
i: number = 0;
}
注:我个人在写组件的过程中,出现过这么一个情况,需要对传入的数组进行解构赋值才可以更新组件,是一样的道理。
与Angular配合的UI框架NG-ZORRO从 7.0 版本开始,NG-ZORRO 组件默认在 OnPush 模式下工作,当你给NG-ZORRO组件传入对象时那就要十分注意上述问题。
如果在子组件中触发一个事件,无论此事件是否和业务有关,甚至是个空事件,那在onPush策略下的子组件也会进行变换检测。代码如下
TimerComponent
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit
} from "@angular/core";
import { DataModel } from "../app.component";
@Component({
selector: "app-timer",
template: `
<div>Counter-timer: {{ timer }}</div>
<button (click)="addi()">Add i on timer</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
timer: number = 0;
addi() {} // 空事件也能触发此组件的变化检测
ngOnInit() {
setInterval(() => {
this.timer++;
console.log(this.timer);
}, 1000);
}
}
AppComponent
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<app-timer></app-timer>
`
})
export class AppComponent {}
代码运行的效果就是,页面上计时器不跳动,但当你点击Add i on timer按钮后,计时器会跳动一下。
ChangeDetectorRef类
ChangeDetectorRef是组件的变化检测器的引用,我们可以通过依赖注入的方式来获取该实例
...
import { ChangeDetectorRef } from "@angular/core";
@Component({...})
export class TimerComponent implements OnInit {
...
constructor(private cdRef: ChangeDetectorRef) {}
...
}
ChangeDetectorRef类包含5个方法
abstract class ChangeDetectorRef {
abstract markForCheck(): void //当组件使用OnPush变更检测策略时,把该组件显式标记为已更改,以便它再次进行检查。
abstract detach(): void //从变更检测树中分离开组件。 已分离的组件在重新附加上去之前不会被检查。 与 detectChanges() 结合使用,可以实现局部变更检测。
abstract detectChanges(): void //检查该组件及其子组件。与 detach 结合使用可以实现局部变更检测。
abstract checkNoChanges(): void //检查变更检测器及其子检测器,如果检测到任何更改,则抛出异常。
abstract reattach(): void //把以前分离开的组件重新附加到变更检测树上。 组件会被默认附加到这棵树上。
}
我们着重说明一下markForCheck()方法,当组件使用OnPush变更检测策略时,遇到markForCheck(),则把此组件标记为已更改,已遍Angular的变化检测能在此组件没有@Input属性更新或事件发生的情况下,也能检测到此组件的变化。见以下代码
TimerComponent
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnInit
} from "@angular/core";
@Component({
selector: "app-timer",
template: `
<div>Counter-timer: {{ timer }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent implements OnInit {
timer: number = 0;
constructor(private cdRef: ChangeDetectorRef) {}
ngOnInit() {
setInterval(() => {
this.timer++;
// 到第三秒时,标记此组件已变化
if (this.timer == 3) {
this.cdRef.markForCheck();
}
console.log(this.timer);
}, 1000);
}
}
AppComponent
import { Component } from "@angular/core";
@Component({
selector: "my-app",
template: `
<app-timer></app-timer>
`
})
export class AppComponent {}
运行代码,你可以看到计时器一开始不跳动,到第三秒的时候,计时器数字跳为3,然后就一直不再跳动。这是因为到第三秒的时候,运行了“this.cdRef.markForCheck();”这句话,标记了TimerComponet已经变化,Angular检测到变化标记后刷新页面数据,并将TimerComponet标记为无变化。所以“this.cdRef.markForCheck();”这句话只能引起一次变化检测,如要多次,则要多次的运行这句话,如果以上代码功能要改成3秒后开始计时,只要将this.timer == 3改成this.timer >= 3就行了。