Angular中ExpressionChangedAfterItHasBeenCheckedError错误及变更检测疑问
场景说明
我阅读了大量文章,但仍对Angular变更检测的部分场景存在疑惑。以下是我的示例:所有组件均实现OnInit、AfterViewInit、DoCheck、AfterViewChecked生命周期钩子的Angular应用,包含Parent、Child、DeepChild三个组件。仅在父组件中实现了常见的加载示例(HTML中使用*ngIf="isLoading"),父组件代码如下:
export class AppComponent implements OnInit, AfterViewInit, DoCheck, AfterViewChecked { isLoading = false; constructor(private changeDetectorRef: ChangeDetectorRef) {} ngAfterViewInit(): void { this.isLoading = true; console.log('parent - after view init'); } ngAfterViewChecked(): void { console.log('AFTER CHECK'); } ngDoCheck(): void { console.log('parent - do check'); } ngOnInit(): void { console.log('parent - init'); } }
未设置this.isLoading = true;时的控制台输出为:
parent - init parent - do check Child - init Child - do check DeepChild - init DeepChild - do check DeepChild - after view init DeepChild - after check Child - after view init Child - after check parent - after view init parent - after view check <with assigning loading value as in code above here I got expression changed error> <and after that I got some more checks generated> parent - do check Child - do check DeepChild - do check DeepChild - after check Child - after check parent - after check
当添加this.isLoading = true;赋值时,会触发ExpressionChangedAfterItHasBeenCheckedError错误,且之后会产生额外的检测日志。针对该场景,我有以下疑问:
- 为何会出现这些额外的检测?
- 我理解该错误是因为视图检测后表达式发生变更,但已有额外检测,为何问题仍存在?
- 若添加
ChangeDetectorRef手动执行detectChanges可解决问题,原因是什么?触发新的检测为何能解决之前的表达式变更问题? - 手动触发
detectChanges仅会触发子组件的doCheck,为何在父组件触发却不会调用父组件自身的检测?
问题解答
1. 为何会出现这些额外的检测?
Angular在开发模式下会执行两轮变更检测:第一轮完成视图更新,第二轮用来校验视图和数据是否一致,防止出现表达式变更后视图未同步的问题。当你在ngAfterViewInit中修改isLoading时,第一轮检测已经完成,此时数据和视图状态不一致,Angular会自动触发额外的一轮检测尝试同步,但这轮检测依然无法解决根本问题,所以会抛出错误。
2. 已有额外检测,为何问题仍存在?
开发模式下的第二轮校验是只读检查,它不会真的更新视图,只是对比数据和当前视图的表达式结果是否一致。当你在ngAfterViewInit修改isLoading时,第一轮检测已经生成了基于isLoading=false的视图,第二轮校验发现表达式结果变了,但它不会执行视图更新,直接抛出错误提示你数据和视图不一致。额外的检测是Angular的自动校验机制,不是用来修复问题的,只是用来暴露问题。
3. 手动执行detectChanges为何能解决问题?
detectChanges会强制触发同步的变更检测循环:当你在ngAfterViewInit中修改isLoading后立即调用detectChanges(),Angular会立刻执行一轮完整的变更检测,更新视图到isLoading=true的状态。等到开发模式的第二轮校验时,数据和视图的表达式结果已经一致了,自然不会再抛出错误。简单说,就是提前手动把视图更新到最新状态,让后续的校验能通过。
4. 父组件触发detectChanges为何不调用自身的检测?
当你在父组件调用detectChanges()时,Angular会从当前组件开始向下遍历所有子组件执行变更检测,但父组件自身的当前检测周期已经完成。ngDoCheck是在变更检测周期开始时触发的,而此时父组件已经走完了自己的检测流程,所以不会再触发父组件的ngDoCheck,只会触发子组件的相关钩子。
内容的提问来源于stack exchange,提问作者ruddnisrus

