请向我解释为什么我一直得到这个错误:ExpressionChangedAfterItHasBeenCheckedError:表达式已经改变后,它被检查。

显然,我只有在开发模式下才会遇到这种情况,在我的产品构建中不会出现这种情况,但这非常烦人,而且我根本不明白在我的开发环境中出现错误而不会在prod上显示的好处——可能是因为我缺乏理解。

通常,修复很简单,我只是把导致错误的代码包装在setTimeout中,就像这样:

setTimeout(()=> {
    this.isLoading = true;
}, 0);

或者使用如下构造函数强制检测更改:

this.isLoading = true;
this.cd.detectChanges();

但是为什么我总是遇到这个错误呢?我想要了解它,这样我就可以在将来避免这些俗套的修复。


当前回答

一旦我理解了Angular生命周期钩子以及它们与变更检测的关系,我就理解了很多。

我试图让Angular更新一个绑定到元素的*ngIf的全局标志,我试图在另一个组件的ngOnInit()生命周期钩子中更改这个标志。

根据文档,这个方法会在Angular检测到变化之后调用:

在第一个ngOnChanges()之后调用一次。

因此,在ngOnChanges()中更新标志不会启动变更检测。然后,一旦变更检测再次自然触发,标志的值就发生了变化,并抛出错误。

在我的例子中,我修改了这个:

constructor(private globalEventsService: GlobalEventsService) {

}

ngOnInit() {
    this.globalEventsService.showCheckoutHeader = true;
}

:

constructor(private globalEventsService: GlobalEventsService) {
    this.globalEventsService.showCheckoutHeader = true;
}

ngOnInit() {

}

它解决了这个问题:)

其他回答

一旦我理解了Angular生命周期钩子以及它们与变更检测的关系,我就理解了很多。

我试图让Angular更新一个绑定到元素的*ngIf的全局标志,我试图在另一个组件的ngOnInit()生命周期钩子中更改这个标志。

根据文档,这个方法会在Angular检测到变化之后调用:

在第一个ngOnChanges()之后调用一次。

因此,在ngOnChanges()中更新标志不会启动变更检测。然后,一旦变更检测再次自然触发,标志的值就发生了变化,并抛出错误。

在我的例子中,我修改了这个:

constructor(private globalEventsService: GlobalEventsService) {

}

ngOnInit() {
    this.globalEventsService.showCheckoutHeader = true;
}

:

constructor(private globalEventsService: GlobalEventsService) {
    this.globalEventsService.showCheckoutHeader = true;
}

ngOnInit() {

}

它解决了这个问题:)

调试技巧

这个错误可能非常令人困惑,而且很容易对它发生的确切时间做出错误的假设。我发现在受影响的组件的适当位置添加大量这样的调试语句很有帮助。这有助于理解流程。

在父类的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),请确保你正在调试你认为你在调试的属性——通过将它设置为一个模糊的颜色值。

我用rxjs解决了这个问题

import { startWith, tap, delay } from 'rxjs/operators';

// Data field used to populate on the html
dataSource: any;

....

ngAfterViewInit() {
  this.yourAsyncData.
      .pipe(
          startWith(null),
          delay(0),
          tap((res) => this.dataSource = res)
      ).subscribe();
}

此错误表明应用程序中存在真正的问题,因此抛出异常是有意义的。

在devMode中,更改检测在每次常规的更改检测运行之后增加一个额外的回合,以检查模型是否已更改。

如果模型在常规变化检测轮和附加变化检测轮之间发生了变化,则这表明也发生了变化

变更检测本身导致了变更 每次调用方法或getter都会返回不同的值

这两个都不好,因为不清楚如何继续,因为模型可能永远不会稳定。

如果Angular一直运行变更检测直到模型稳定,那么它可能会一直运行下去。 如果Angular不运行变更检测,那么视图可能不会反映模型的当前状态。

参见Angular2中生产模式和开发模式的区别是什么?

在@ViewChild装饰器中添加static: true

@ViewChild(UploadComponent, { static: true }) uploadViewChild: UploadComponent;