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

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

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

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

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

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

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


当前回答

我在RxJS/Observables和静态模拟数据之间有这个问题。首先,我的应用程序使用静态模拟数据,在我的例子中是数据数组

html是这样的:

*ngFor="let x of myArray?.splice(0, 10)"

我们的想法是只显示myArray中的10个元素。Splice()获取原始数组的副本。据我所知,这在Angular中是完全没问题的。

然后我把数据流改为Observable模式,因为我的“真实”数据来自Akita(一个状态管理库)。这意味着我的html变成:

*ngFor="let x of (myArray$ | async)?.splice(0, 10)"

where myArray$是[was]类型的Observable<MyData[]>,这个模板中的数据操作是导致错误发生的原因。不要对RxJS对象这样做。

其他回答

我的问题是我在加载这个对象时打开了一个Ngbmodal弹出框,这个对象在被检查后被改变了。我能够通过打开setTimeout内的模态弹出框来解决它。

setTimeout(() => {
  this.modalReference = this.modalService.open(this.modal, { size: "lg" });
});

如果你在ngAfterViewInit()中触发EventEmitter时看到ExpressionChangedAfterItHasBeenCheckedError,那么你可以在创建EventEmitter时传递true,使其在当前更改检测周期后异步发射。

@Component({
  ...
})
export class MyComponent implements AfterViewInit {
  // Emit asynchronously to avoid ExpressionChangedAfterItHasBeenCheckedError
  @Output() valueChange = new EventEmitter<number>(true);

  ...

  ngAfterViewInit(): void {
    ...
    this.valueChange.emit(newValue);
    ...
  }

}

参考文章https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4

因此,变更检测背后的机制实际上是以同步执行变更检测和验证摘要的方式工作的。这意味着,如果我们异步更新属性,当验证循环运行时,值不会被更新,我们也不会得到ExpressionChanged…错误。我们得到这个错误的原因是,在验证过程中,Angular看到的值与它在变更检测阶段记录的值不同。所以为了避免....

1)使用changeDetectorRef

2)使用setTimeOut。这将在另一个VM中作为宏任务执行您的代码。Angular在验证过程中不会看到这些更改,你也不会得到这个错误。

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

3)如果你真的想在相同的虚拟机上执行你的代码使用

Promise.resolve(null).then(() => this.isLoading = true);

这将创建一个微任务。微任务队列是在当前同步代码完成执行之后处理的,因此对属性的更新将在验证步骤之后发生。

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/

我也遇到过类似的问题。查看生命周期钩子文档,我将ngAfterViewInit更改为ngAfterContentInit,它工作正常。