ngDoCheck 的执行时机
在状态发生变化,angular 自己本身不能捕获这个变化时会触发 NgDoCheck。
每次变化检测以后,都会触发 ngDoCheck 钩子函数,紧跟在 ngOnChanges 和 ngOnInit 之后运行。
在这种情况下没有@Input 绑定,所以 ngOnChanges 不会被触发,那为什么组件 B 和 C 的 ngDoCheck 分别执行了两遍
对于 ngOnChanges 生命周期:
当 Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges 对象
在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。
在父组件执行变化检测,会更新子组件的绑定,从而触发一次子组件的 ngDoCheck;第二遍是子组件它本身的变化检测触发的。
A 组件包含 B 组件;B 组件包含 C 组件,他们的检测过程如下
Checking A component:
- update B input bindings
- call NgDoCheck on the B component
- update DOM interpolations for component A
Checking B component:
- update C input bindings
- call NgDoCheck on the C component
- update DOM interpolations for component B
Checking C component:
- update DOM interpolations for component C
- 设置组件的检测策略是 OnPush
import { Component, OnChanges, DoCheck, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: 'app-componentb',
template: `<h3>this is component b</h3>
<app-componentc></app-componentc>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
将组件 B 设置为 OnPush 策略
此时的检测规则:
没有@Input 绑定、没有 DOM 事件、没有 Observable、没有手动触发变化检测,那么 angular 只会对组件 A 执行变化检测,跳过组件 B 和 C 的变化检测
但是结果却是:
很奇怪对不对!!本来说好的组件 B 和 C 不会执行变化检测,怎么 NgDoCheck 还是触发了?组件 B 中的 ngDoCheck 执行了两遍!!
B check
C check
B check
B 组件设置了 OnPush 时,此时 angular 的检测策略
Checking A component:
- update B input bindings
- call NgDoCheck on the B component
- update DOM interpolations for component A
if (bindings reference changed
|| DOM event
|| Observable Async pipe
|| ChangeDetectorRef.detectChanges()
|| ChangeDetectorRef.markForCheck()
|| ApplicationRef.tick()) -> checking B component:
- update C input bindings
- call NgDoCheck on the C component
- update DOM interpolations for component B
Checking C component:
- update DOM interpolations for component C
解释:
组件 B 执行第一个 NgDoCheck 很好理解:子组件里的 NgDoCheck 每次都是在父组件执行变化检测的时候执行,组件 B 虽然设置了 OnPush 策略,但是父组件 A 的在执行变化检测时会触发一次 B 的 NgDoCheck。
组件 B 执行第二个 NgDoCheck 是因为:组件 C 在它自己的 NgDoCheck 之后会 update DOM interpolations for component B,组件 B 状态发生改变,因为此时组件 B 设置了 OnPush,不会执行变化检测,angular 捕获不到这个状态改变。NgDoCheck 会在状态发生变化,angular 自己又不能捕获时被触发。
组件 C 执行 NgDoCheck 触发的原因和组件 B 的第二个 NgOnCheck 类似:update DOM interpolations for component C 组件 C 本身自己页面渲染时状态发生改变,因为组件 B 被设置为 OnPush 策略,子组件 C 也是 OnPush 策略,angular 捕获不到这个状态改变。NgDoCheck 会在状态发生变化,angular 自己又不能捕获时被触发。
- 这种情况就解释了 NgDoCheck 执行的第一种情景:在状态发生变化,angular 自己本身不能捕获这个变化时会触发 NgDoCheck
angular 的变更检测策略
angular 默认的变化检测机制是 ChangeDetectionStrategy.Default:异步事件 callback 结束后,NgZone 会触发整个组件树至上而下做变化检测
- OnPush 策略
angular 除了默认的变化检测机制,也提供了 ChangeDetectionStrategy.OnPush,用 OnPush 可以跳过某个 component 或者某个父组件以及它下面所有子组件的变化检测
在组件中加了 OnPush 表示,在发生异步事件以后触发变化检测,angular 会跳过这个组件,不会触发这个组件的变化检测。如果 OnPush 是加在某个父组件上,那么这个父组件和它下面所有的子组件都不会触发变化检测。
某一个组件中设置 angular 变化检测策略为 OnPush,如果没有以下四种情况,angular 是不会为这个组件或者它的子组件执行变化检测
组件的@Input()引用发生变化。
组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down 等事件。
Observable 订阅事件,同时设置 Async pipe。
ChangeDetectorRef.detectChanges()、ChangeDetectorRef.markForCheck()、ApplicationRef.tick(),手动调用这三种方式触发变化检测。
- @Input 的属性发生改变
必须是@Input 的引用发生改变才会触发变化检测,并且仅限于@Input 的变化检测,在 OnPush 策略下,会触发组件的变化检测。
- 组件的 DOM 事件
组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down 等事件,在 OnPush 策略下,会触发组件的变化检测。
- ChangeDetectorRef.detectChanges()
在值改变的地方调用,进行检测
eg:
constructor(private cd: ChangeDetectorRef) { }
ngOnInit() {
setInterval(() => {
this.counter = this.counter + 5;
this.cd.detectChanges();
}, 1000);
}
- ChangeDetectorRef.markForCheck()
在值改变的地方调用,进行检测
constructor(private cd: ChangeDetectorRef) { }
ngOnInit() {
setInterval(() => {
this.counter = this.counter + 10;
this.cd.markForCheck();
}, 1000);
}
效果跟 detectChanges 是一样的,只不过 detectChanges 会立马触发当前组件和它子组件变化检测。markForCheck 并不会立马触发变化检测,而是标记需要被变化检测,在当前或下一轮的变化检测中被触发。
- ApplicationRef.tick()
constructor(private applicationRef: ApplicationRef)) { }
ngOnInit() {
setInterval(() => {
this.counter = this.counter + 20;
this.applicationRef.tick();
}, 1000);
}
ApplicationRef.tick()触发整个应用的组件树从上到下执行变化检测。
- 总结:
在实际应用开发过程中,应该尽量遵循以下的原则:
尽量使用 OnPush 策略从叶节点组件开始来优化整个应用的性能
尽量多结合使用 OnPush 和 async pipe
angular 变化检测机制
变化检测都是沿着组件树从 root component 开始至上而下执行的。
我们都知道在 angular 里,每个 component 都有一个 html 模板,在 angular 内部,编译器在 component 和模板之间会生成一个 component view。数据绑定、脏数据检查和更新 DOM 都是由这个 component view 实现的。变化检测机制也可以说就是沿着 component view 的树状结构从上到下执行的。
angular 需要在 component view 保存每个 DOM 节点引用,同时也要保存 component 数据引用、数据之前的值和取值表达式
- angular 通常有如下三种方式会导致组件数据变化:
事件:页面 click、submit、mouse down……
XHR:从后端服务器拿到数据
Timers:setTimeout()、setInterval()
- 前面那三种方式会导致 angular 状态变化,那又是谁知道状态已经发生改变,需要通知 angular 触发变化检测从而更新页面 DOM 呢?NgZone(zone.js)充当了这个角色。
NgZone 可以简单的理解为是一个异步事件拦截器,它能够 hook 到异步任务的执行上下文,然后就可以来处理一些操作,比如每个异步任务 callback 以后就会去通知 angular 做变化检测
angular 源码中有一个 ApplicationRef,可以监听 NgZones onTurnDone 事件,每当 onTurnDone 被触发后,它会立马执行 tick()方法,tick()会从上到下沿着组件树触发变化检测。ApplicationRef 简洁版代码如下
// very simplified version of actual source
class ApplicationRef {
changeDetectorRefs:ChangeDetectorRef[] = [];
constructor(private zone: NgZone) {
this.zone.onTurnDone
.subscribe(() => this.zone.run(() => this.tick());
}
tick() {
this.changeDetectorRefs
.forEach((ref) => ref.detectChanges());
}
}
有了 NgZone 上述三种异步事件都会导致整个 angular 应用发生变化检测,虽然 angular 变化检测本身性能已经很好了,在毫秒内可以做成百上千次变化检测。但是随着项目越来越大,其实很多不必要的变化检测还是会在一定程度上影响性能.解决方式 OnPush 策略