请向我解释为什么我一直得到这个错误:ExpressionChangedAfterItHasBeenCheckedError:表达式已经改变后,它被检查。
显然,我只有在开发模式下才会遇到这种情况,在我的产品构建中不会出现这种情况,但这非常烦人,而且我根本不明白在我的开发环境中出现错误而不会在prod上显示的好处——可能是因为我缺乏理解。
通常,修复很简单,我只是把导致错误的代码包装在setTimeout中,就像这样:
setTimeout(()=> {
this.isLoading = true;
}, 0);
或者使用如下构造函数强制检测更改:
this.isLoading = true;
this.cd.detectChanges();
但是为什么我总是遇到这个错误呢?我想要了解它,这样我就可以在将来避免这些俗套的修复。
当值在同一个更改检测周期中更改多次时,将发生此错误。我在一个TypeScript getter中遇到了这个问题,它的返回值经常变化。要解决这个问题,您可以限制一个值,使其在每个更改检测周期中只能更改一次,如下所示:
import { v4 as uuid } from 'uuid'
private changeDetectionUuid: string
private prevChangeDetectionUuid: string
private value: Date
get frequentlyChangingValue(): any {
if (this.changeDetectionUuid !== this.prevChangeDetectionUuid) {
this.prevChangeDetectionUuid = this.changeDetectionUuid
this.value = new Date()
}
return this.value
}
ngAfterContentChecked() {
this.changeDetectionUuid = uuid()
}
HTML:
<div>{{ frequentlyChangingValue }}</div>
这里的基本方法是每个变更检测周期都有自己的uuid。当uuid改变时,您就知道您已经进入了下一个循环。如果循环已经改变,则更新值并返回它,否则只需返回与之前在此循环中返回的值相同的值。
这确保每个循环只返回一个值。这对于频繁更新值很有效,因为更改检测周期发生得非常频繁。
为了生成uuid,我使用了uuid npm模块,但你可以使用任何方法来生成唯一的随机uuid。
此错误表明应用程序中存在真正的问题,因此抛出异常是有意义的。
在devMode中,更改检测在每次常规的更改检测运行之后增加一个额外的回合,以检查模型是否已更改。
如果模型在常规变化检测轮和附加变化检测轮之间发生了变化,则这表明也发生了变化
变更检测本身导致了变更
每次调用方法或getter都会返回不同的值
这两个都不好,因为不清楚如何继续,因为模型可能永远不会稳定。
如果Angular一直运行变更检测直到模型稳定,那么它可能会一直运行下去。
如果Angular不运行变更检测,那么视图可能不会反映模型的当前状态。
参见Angular2中生产模式和开发模式的区别是什么?
setTimeout或delay(0)如何解决这个问题?
以下是上面的代码修复问题的原因:
The initial value of the flag is false, and so the loading indicator will NOT be displayed initially
ngAfterViewInit() gets called, but the data source is not immediately called, so no modifications of the loading indicator will be made synchronously via ngAfterViewInit()
Angular then finishes rendering the view and reflects the latest data changes on the screen, and the Javascript VM turn completes
One moment later, the setTimeout() call (also used inside delay(0)) is triggered, and only then the data source loads its data
the loading flag is set to true, and the loading indicator will now be displayed
Angular finishes rendering the view, and reflects the latest changes on the screen, which causes the loading indicator to get displayed
这一次没有发生错误,因此这修复了错误消息。
来源:https://blog.angular-university.io/angular-debugging/
调试技巧
这个错误可能非常令人困惑,而且很容易对它发生的确切时间做出错误的假设。我发现在受影响的组件的适当位置添加大量这样的调试语句很有帮助。这有助于理解流程。
在父类的put语句中(确切的字符串'EXPRESSIONCHANGED'很重要),但除此之外,这些只是例子:
console.log('EXPRESSIONCHANGED - HomePageComponent: constructor');
console.log('EXPRESSIONCHANGED - HomePageComponent: setting config', newConfig);
console.log('EXPRESSIONCHANGED - HomePageComponent: setting config ok');
console.log('EXPRESSIONCHANGED - HomePageComponent: running detectchanges');
在子/ services / timer回调函数中:
console.log('EXPRESSIONCHANGED - ChildComponent: setting config');
console.log('EXPRESSIONCHANGED - ChildComponent: setting config ok');
如果你手动运行detectChanges,添加日志记录:
console.log('EXPRESSIONCHANGED - ChildComponent: running detectchanges');
this.cdr.detectChanges();
然后在Chrome调试器只是过滤'EXPRESSIONCHANGES'。这将显示所有设置的流程和顺序,以及Angular抛出错误的确切位置。
您还可以单击灰色链接来放入断点。
另一件需要注意的事情是,如果你在整个应用程序中都有类似命名的属性(比如style.background),请确保你正在调试你认为你在调试的属性——通过将它设置为一个模糊的颜色值。
当我添加*ngIf时,我的问题很明显,但这不是原因。该错误是由于在{{}}标签中更改模型,然后稍后尝试在*ngIf语句中显示更改后的模型而导致的。这里有一个例子:
<div>{{changeMyModelValue()}}</div> <!--don't do this! or you could get error: ExpressionChangedAfterItHasBeenCheckedError-->
....
<div *ngIf="true">{{myModel.value}}</div>
为了解决这个问题,我将调用changeMyModelValue()的位置更改为更有意义的位置。
在我的情况下,我希望每当子组件更改数据时调用changeMyModelValue()。这要求我在子组件中创建并发出一个事件,以便父组件可以处理它(通过调用changeMyModelValue())。看到https://angular.io/guide/component-interaction parent-listens-for-child-event