Introduction ngDoCheck
runs before every time Angular checks a component’s template for changes.
常见的误解 很多人误以为,只要这个函数调用了,那么就证明Angular对当前组件进行了变更检测,这是一个常见的误解。注意看这个函数的定义 :它是在每次Angular检查组件的模板变化之前运行的。所以不能以这个函数的调用作为Angular进行了变更检测的依据。
我们来看一个实际的例子:
一个父组件ParentComponent
, 采用ChangeDetectionStrategyDefault
模式
一个子组件ChildComponent
,采用ChangeDetectionStrategy.OnPush
模式
当父组件处理点击事件时,子组件的ngDoCheck
函数会被调用吗?
再添加一个孙子组件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 @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 @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 @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
函数。但是我在网上找到了一些资料,可以参考一下:
使用第三方库时,如果你的组件中使用了第三方库,而第三方库改变了DOM状态,但是Angular没有检测到,这时可以使用ngDoCheck
函数来手动检测状态变化。
自定义变更检测的逻辑,比如在某些条件下不进行变更检测,可以在ngDoCheck
函数中实现。
子组件接收一个对象作为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 (); } else { this .cdf .reattach (); } } }
注意:这个例子十分牵强,现实中不会有这种情况的。
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
https://angular.dev/guide/components/lifecycle#ngdocheck
https://stackoverflow.com/a/45522199/1487475
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 .