「Expression has changed after it was checked」—— 为什么会发生以及如何应对?
\n\n如果您曾经用 Angular 编写过应用程序,那么您一定在控制台中遇到过这个可怕的消息:
\n\nERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.\n\n这个错误是开发者的真正「噩梦」。Stack Overflow 上有成千上万个问题,数百小时的调试,数十次精神崩溃。但好消息是:您不仅可以修复它,还可以理解它发生的原因。在本文中,我们将深入分析 Expression has changed after it was checked 错误的主要原因,用 TypeScript 编写示例,并提供具体的解决方案。开始吧!
\n\n这个错误到底意味着什么?
\n\nAngular 在两种模式下运行:开发模式(dev mode)和生产模式。在开发模式下,框架会执行额外的检查——在每个变更检测周期之后,它会运行第二次遍历,以确保数据没有「实时」改变。如果值在第一次和第二次遍历之间发生了变化——Angular 就会抛出 ExpressionChangedAfterItHasBeenCheckedError 错误。
\n\n简单来说:您告诉 Angular「数据是这样的」,它检查了,然后您又更改了数据。Angular 会感到不满并报错。
\n\n错误的三大主要原因
\n\n1. 在 ngAfterViewInit 之后的生命周期钩子中更改数据
\n\n最常见的原因——在 ngAfterViewInit、ngAfterContentInit 或 ngAfterViewChecked 钩子中尝试更改组件的属性。此时 Angular 已经完成了视图检查,任何更改都会引发错误。
TypeScript 示例:
\n\nimport { Component, AfterViewInit } from '@angular/core';\n\n@Component({\n selector: 'app-example',\n template: '<p>{{ message }}</p>'\n})\nexport class ExampleComponent implements AfterViewInit {\n message = '初始值';\n\n ngAfterViewInit(): void {\n // 错误!在视图检查后更改属性\n this.message = '更改后的值';\n }\n}\n\n这里 Angular 首先检查 message(值为「初始值」),然后调用 ngAfterViewInit,您在钩子中将其更改为「更改后的值」。在第二次检查时,Angular 发现不一致并抛出错误。
2. 生命周期内的异步操作
\n\n如果您在钩子中启动异步代码(setTimeout、Promise、Observable),则数据更新发生在 Angular 控制区域之外。
\n\n示例:
\n\nimport { Component, OnInit } from '@angular/core';\n\n@Component({\n selector: 'app-async',\n template: '<p>{{ data }}</p>'\n})\nexport class AsyncComponent implements OnInit {\n data = '开始';\n\n ngOnInit(): void {\n setTimeout(() => {\n this.data = '新值';\n }, 0);\n }\n}\n\n尽管延迟为 0 毫秒的 setTimeout 几乎立即执行,但 Angular 已经完成了检查。在开发模式下,这会导致错误。
\n\n3. 嵌套组件和通过 Input 传递数据
\n\n当父组件通过 @Input 向子组件传递数据,而子组件试图在其生命周期中更改这些数据时,就会发生冲突。
示例:
\n\n// 父组件\n@Component({\n selector: 'app-parent',\n template: '<app-child [value]="parentValue"></app-child>'\n})\nexport class ParentComponent {\n parentValue = '原始值';\n}\n\n// 子组件\n@Component({\n selector: 'app-child',\n template: '<p>{{ value }}</p>'\n})\nexport class ChildComponent implements AfterViewInit {\n @Input() value: string = '';\n\n ngAfterViewInit(): void {\n this.value = '在子组件中更改的值';\n }\n}