0%

angular-lifecyle-ondocheck

Introduction

ngDoCheck runs before every time Angular checks a component’s template for changes.

常见的误解

很多人误以为,只要这个函数调用了,那么就证明Angular对当前组件进行了变更检测,这是一个常见的误解。注意看这个函数的定义:它是在每次Angular检查组件的模板变化之前运行的。所以不能以这个函数的调用作为Angular进行了变更检测的依据。

我们来看一个实际的例子:

  1. 一个父组件ParentComponent, 采用ChangeDetectionStrategyDefault模式
  2. 一个子组件ChildComponent,采用ChangeDetectionStrategy.OnPush模式
  3. 当父组件处理点击事件时,子组件的ngDoCheck函数会被调用吗?
  4. 再添加一个孙子组件GrandChildComponent,采用ChangeDetectionStrategy.OnPush模式,当父组件处理点击事件时,孙子组件的ngDoCheck函数会被调用吗?

ParentComponent采用默认的ChangeDetectionStrategyDefault模式,并且添加了一个按钮,当我们点击这个按钮时,Angular会触发变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// parent.component.ts
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
onParentButtonClick() {
console.log(`click in ParentComponent`);
}
}

ChildComponent采用ChangeDetectionStrategy.OnPush模式,只有当Input属性发生变化时,或者响应自身事件,或者手动触发了变更检测时,ngDoCheck函数才会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// child.component.ts
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<app-grand-child />
`,
styleUrl: './child.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements DoCheck {
ngDoCheck(): void {
console.log(`ngDoCheck in ChildComponent`);
}
}

GrandChildComponent同样采用了ChangeDetectionStrategy.OnPush模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// grand-child.component.ts
@Component({
selector: 'app-grand-child',
standalone: true,
imports: [],
template: `<p>grand-child works!</p>`,
styleUrl: './grand-child.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GrandChildComponent implements DoCheck {
ngDoCheck(): void {
console.log(`ngDoCheck in GrandChildComponent`);
}
}

当我们点击Parent按钮时,控制台输出包含如下一行,说明ChildComponent的ngDoCheck函数被调用了。

1
ngDoCheck in ChildComponent

可是我们明明指定了ChangeDetectionStrategy.OnPush模式,为什么会调用ngDoCheck函数呢?难道OnPush模式失效了吗?

其实不然,正如前面所说的,ngDoCheck函数是在每次Angular检查组件的模板变化之前运行的。所以,即使ChildComponent采用了ChangeDetectionStrategy.OnPush模式,ngDoCheck也被调用了,但是这并不意味着Angular对ChildComponent进行了变更检测。

这种情况只发生在OnPush根组件上,上面的GrandChildComponent并没有被调用,因为它是ChildComponent的子组件,所以它的ngDoCheck不会调用。

有此类行为的生命周期函数还有ngAfterViewChecked,无论Angular是否进行了变更检测,这个函数都会被调用。

如何确定Angular是否进行了变更检测?

对于一个组件来说,我如何确定Angular是否对它进行了变更检测呢?这个问题,其实困扰了我很久,以前我一直以为ngDoCheck函数的调用就是Angular进行了变更检测的标志,由上面的结论可知,这是不准确的。而其他生命周期函数也无法准确的告诉我们Angular是否进行了变更检测。

真的没办法了吗?

有的!其实之所以有这个困惑,还是对Angular变更检测理解不够深入,Angular的变更检测到底做了什么?其中必然有一个步骤是对template进行检查,如果template中绑定的值发生了变化,那么Angular就会更新视图。所以,我们可以通过template是否发生变化来判断Angular是否进行了变更检测。

代码很简单,只要在ChildComponent的模板中插入一个随机值即可,如果Angular进行了变更检测,那么每次这个值都会变化。如果这个值没有变,那么Angular就没有进行变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works! {{Math.random()}}</p>
<app-grand-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})

这时我们再点击Parent按钮,发现ChildComponent的模板中的随机值是不变的,这证明Angular没有进行变更检测。

ngDoCheck到底怎么用?

其实ngDoCheck是在Angular进行变更检测之前给用户一个机会,执行一些自定义逻辑。注意看官网的这句话:

1
You can use this lifecycle hook to manually check for state changes outside of Angular's normal change detection, manually updating the component's state.

所以到底要怎么使用它?恕我经验不够,我至今还未在项目中实际使用过ngDoCheck函数。但是我在网上找到了一些资料,可以参考一下:

  1. 使用第三方库时,如果你的组件中使用了第三方库,而第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck函数来手动检测状态变化。
  2. 自定义变更检测的逻辑,比如在某些条件下不进行变更检测,可以在ngDoCheck函数中实现。
  3. 子组件接收一个对象作为Input属性,而父组件只改变了对象的属性,这时ngOnChanges函数不会被调用的,这时可以使用ngDoCheck函数来检测对象的属性变化。
    说实话,这个例子非常的牵强,父组件只该变对象的一个属性这不是好的编程习惯,这种情况应该直接传递一个新的对象给子组件。但是作为例子,我们还是说一下这个情况。

使用第三方库

组件中使用第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck函数来手动检测状态变化。

1
2
3
ngDoCheck() {
this.cdf.markForCheck();
}

自定义变更检测逻辑

假设有一个父组件ParentComponent,一个子组件ChildComponent,父组件给子组件传递一个User对象,我们将User的年龄显示到子组件页面上,我们希望达到一个效果,如果年龄小于50岁时,子组件不进行变更检测,这种情况就需要使用ngDoCheck函数。

父组件定义如下,注意在onParentButtonClick函数中要重新赋值一个新的User对象,而不是改变User对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child [user]="user" />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
user: User = {
id: 1, name: 'Philip', age: 40,
}

onParentButtonClick() {
const age = Math.floor(Math.random() * 100) + 1;
this.user = {...this.user, age};
}
}

子组件定义如下,我们需要在ngDoCheck中自定义变更检测的逻辑。(注意,ngDoCheck在每次ngOnChanges之后调用。), 如果年龄小于50岁时,我们是用ChangeDetectorRef.detach()函数来停止变更检测,大于50岁时,我们使用ChangeDetectorRef.reattach()函数来恢复变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<p>{{user?.age}}</p>
<app-grand-child/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements DoCheck, OnChanges {
@Input() user: User | null = null;
currentAge: number | null = null;

constructor(private cdf: ChangeDetectorRef) {
}

ngOnChanges(changes: SimpleChanges): void {
this.currentAge = changes['user'].currentValue.age;
}

ngDoCheck(): void {
if (this.currentAge! < 50) {
this.cdf.detach(); // stop change detection
} else {
this.cdf.reattach(); // restore change detection
}
}
}

注意:这个例子十分牵强,现实中不会有这种情况的。

子组件接收对象作为Input属性,父组件只改变了对象的属性

User类型定义:

1
2
3
4
5
export interface User {
id: number;
name: string;
age: number;
}

父组件定义如下,初始化时,我们传递一个user对象给ChildComponent, 点击按钮时,改变user对象的age属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component({
selector: 'app-parent',
standalone: true,
imports: [
ChildComponent
],
template: `
<p>parent works!</p>
<app-child [user]="user" />
<button (click)="onParentButtonClick()">Parent</button>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
user: User = {
id: 1, name: 'Philip', age: 40,
}

onParentButtonClick() {
this.user.age = 18;
}
}

子组件定义如下,当父组件点击按钮时,子组件的页面上,age值并没有变化,还是40.
子组件的ngDoCheck函数会被调用,我们可以在ngDoCheck函数中检测user对象的属性变化,并打印出变化的值。
但是子组件的ngOnChanges函数不会被调用,因为父组件只改变了user对象的属性,而没有改变user对象本身。而ngOnChanges比较的是对象的引用,而不是对象内部的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component({
selector: 'app-child',
standalone: true,
imports: [
GrandChildComponent
],
template: `
<p>child works!</p>
<p>{{user?.age}}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements OnInit, DoCheck, OnChanges {
@Input() user: User | null = null;
differ: any;

constructor(private differs: KeyValueDiffers) {
}

ngOnInit() {
this.differ = this.differs.find(this.user).create();
}

ngOnChanges(changes: SimpleChanges): void {
console.log(changes[0].currentValue.age);
}

ngDoCheck(): void {
console.log(`ngDoCheck in ChildComponent`);
const userChanges = this.differ.diff(this.user);
if (userChanges) {
userChanges.forEachChangedItem((changeRecord: any) => {
console.log('item changed : ' + changeRecord.key + ' ' + JSON.stringify(changeRecord.currentValue))
});
userChanges.forEachAddedItem((changeRecord: any) => {
console.log('item added : ' + changeRecord.key + ' ' + JSON.stringify(changeRecord.currentValue))
});
}
}
}

如果我们想让子组件页面上的age值变化,那么只需要手动触发一次变更检测即可。此时再点击父组件中的按钮,子组件的age值会变为18。

1
2
3
4
5
constructor(private cdf: ChangeDetectorRef) {}

ngDoCheck(): void {
this.cdf.markForCheck();
}

References

  1. https://angular.dev/guide/components/lifecycle#ngdocheck
  2. https://stackoverflow.com/a/45522199/1487475
  3. https://medium.com/@tehseen_ullah786/here-is-why-we-should-use-ngdocheck-in-angular-28bc98a86d85#:~:text=Integration%20with%20third%2Dparty%20libraries,and%20updates%20the%20view%20accordingly.