在 Angular官方文档 对ngDoCheck
执行时机是这么描述的:
Detect and act upon changes that Angular can't or won't detect on its own.
Called during every change detection run, immediately after ngOnChanges() and ngOnInit().
直译过来就是:
-
在状态发生变化,Angular 自己本身不能捕获这个变化时会触发
NgDoCheck
。 -
每次变化检测以后,都会触发
ngDoCheck
钩子函数,紧跟在ngOnChanges
和ngOnInit
之后运行。
第二点很好理解,第一点到底是什么意思呢?
在【Angular Change Detection:变化检测策略】这篇文章中解释过:某一个组件中设置 Angular 变化检测策略为 OnPush,如果没有以下四种情况,Angular 是不会为这个组件或者它的子组件执行变化检测。
- 组件的 @Input() 引用发生变化。
- 组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down 等事件。
- Observable 订阅事件,同时设置 Async pipe。
- ChangeDetectorRef.detectChanges()、ChangeDetectorRef.markForCheck()、ApplicationRef.tick(),手动调用这三种方式触发变化检测。
我们来看一个例子,有一个组件树结构如下:
这三个组件代码如下:
//Component A
import { Component } from "@angular/core";
@Component({
template: `<h1>this is component a</h1>
<app-componentb></app-componentb>`
})
export class AComponent {
}
//Component B
import { Component, OnChanges, DoCheck } from "@angular/core";
@Component({
selector: 'app-componentb',
template: `<h3>this is component b</h3>
<app-componentc></app-componentc>`
})
export class BComponent implements OnChanges, DoCheck {
ngOnChanges() {
console.log('ngOnChanges triggered in component B');
}
ngDoCheck() {
console.log('ngDoCheck triggered in component B');
}
}
//Component C
import { Component, OnChanges, DoCheck } from "@angular/core";
@Component({
selector: 'app-componentc',
template: `<h5>this is component C</h5>`
})
export class CComponent implements OnChanges, DoCheck {
ngOnChanges() {
console.log('ngOnChanges triggered in component C');
}
ngDoCheck() {
console.log('ngDoCheck triggered in component C');
}
}
第一种情况:三个组件都是默认的检测策略.
没有@Input
绑定、没有 DOM 事件、没有Observable
、没有手动触发变化检测,页面运行起来以后,效果如下:
在这种情况下没有@Input
绑定,所以ngOnChanges
不会被触发,那为什么组件B和C的ngDoCheck
分别执行了两遍?
其实在【Angular:单向数据流】这篇文章中解释过:整个变化检测和组件生命周期钩子函数的执行顺序之间的关系。对于组件A 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
在父组件执行变化检测,会更新子组件的绑定,从而触发一次子组件的ngDoCheck
;第二遍是子组件它本身的变化检测触发的。
这种情况就解释了 NgDoCheck 执行的第二种情景:每次变化检测以后,都会触发 ngDoCheck 钩子函数,紧跟在 ngOnChanges 和 ngOnInit 之后运行
第二种情况:把 ComponentB 的变化检测策略设置为 OnPush
没有@Input
绑定、没有 DOM 事件、没有Observable
、没有手动触发变化检测,那么 Angular 只会对组件A执行变化检测,跳过组件 B 和 C 的变化检测,代码如下:
// Component B with ChangeDetectionStrategy.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
})
export class BComponent implements OnChanges, DoCheck {
ngOnChanges() {
console.log('ngOnChanges triggered in component B');
}
ngDoCheck() {
console.log('ngDoCheck triggered in component B');
}
}
页面运行起来后效果如下:
很奇怪对不对!!本来说好的组件 B 和 C 不会执行变化检测,怎么NgDoCheck
还是触发了?组件 B 中的ngDoCheck
执行了两遍!!
我们来看下在组件 B 中设置了OnPush
检测策略,整个变化检测流程:
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
总结
ngDoCheck 不只是在变化检测后触发,在组件里设置 OnPush 策略,ngDoCheck 依旧可以被触发。
本文中用到到的示例代码在这里:angular-change-detection