ExpressionChangedAfterItHasBeenCheckedError解释

请向我解释为什么我一直得到这个错误:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

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

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

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

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

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

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

429495 次浏览

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

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

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

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

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

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

参见在Angular2中,生产模式和开发模式有什么不同?

更新

我强烈建议首先从OP的自我反应开始:适当地考虑在constructor中可以做什么,而在ngOnChanges()中应该做什么。

原始

这与其说是一个答案,不如说是一个题外话,但它可能会帮助到一些人。当我试图让按钮的存在取决于表单的状态时,我偶然发现了这个问题:

<button *ngIf="form.pristine">Yo</button>

据我所知,这种语法会导致根据条件从DOM中添加和删除按钮。这反过来又导致了ExpressionChangedAfterItHasBeenCheckedError

在我的情况下修复(尽管我不声称掌握了差异的全部含义),是使用display: none代替:

<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>

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

我在Ionic3(它使用Angular 4作为其技术堆栈的一部分)中遇到过这种错误。

对我来说,它是这样的:

# EYZ0

所以我试图有条件地改变ion-icon的类型从pinremove-circle,每个模式的屏幕上正在操作。

我猜我将不得不添加一个*ngIf代替。

我面临着同样的问题,因为我的组件中的一个数组的值发生了变化。但是我没有检测值变化上的变化,而是将组件变化检测策略更改为onPush(它将检测对象变化上的变化,而不是值变化上的变化)。

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';


@Component({
changeDetection: ChangeDetectionStrategy.OnPush
selector: -
......
})

一旦我理解了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() {


}

它解决了这个问题:)

在我的例子中,我在运行测试时在规范文件中遇到了这个问题。

我不得不改变ngIf [hidden]

<app-loading *ngIf="isLoading"></app-loading>

<app-loading [hidden]="!isLoading"></app-loading>

对于我的问题,我正在阅读github -“在afterViewInit中更改组件“非模型”值时,ExpressionChangedAfterItHasBeenCheckedError”,并决定添加ngModel

<input type="hidden" ngModel #clientName />

它解决了我的问题,我希望它能帮助到别人。

请遵循以下步骤:

< p > 1。 通过从@angular/core中导入'ChangeDetectorRef'来使用它,如下所示
import{ ChangeDetectorRef } from '@angular/core';
< p > 2。 在constructor()中实现如下
constructor(   private cdRef : ChangeDetectorRef  ) {}
< p > 3。 添加下面的方法到你的函数,你正在调用的事件,如点击按钮。它是这样的:

functionName() {
yourCode;
//add this line to get rid of the error
this.cdRef.detectChanges();
}

以下是我对正在发生的事情的看法。我还没有阅读文档,但我确信这是错误显示的部分原因。

*ngIf="isProcessing()"

当使用*ngIf时,它会在每次条件改变时通过添加或删除元素来物理地改变DOM。因此,如果条件在呈现给视图之前发生了变化(这在Angular的世界中是极有可能的),就会抛出错误。请参阅开发和生产模式之间的解释在这里

[hidden]="isProcessing()"

当使用[hidden]时,它不会在物理上改变DOM,而只是从视图中隐藏element,很可能在后面使用CSS。元素仍然在DOM中,但不可见,这取决于条件的值。这就是为什么使用[hidden]时不会发生错误的原因。

参考文章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);

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

@HostBinding可能是这个错误的一个令人困惑的来源。

例如,假设您在组件中有以下主机绑定

// image-carousel.component.ts
@HostBinding('style.background')
style_groupBG: string;

为了简单起见,我们假设这个属性是通过以下输入属性更新的:

@Input('carouselConfig')
public set carouselConfig(carouselConfig: string)
{
this.style_groupBG = carouselConfig.bgColor;
}

在父组件中,您通过编程方式在ngAfterViewInit中设置它

@ViewChild(ImageCarousel) carousel: ImageCarousel;


ngAfterViewInit()
{
this.carousel.carouselConfig = { bgColor: 'red' };
}

事情是这样的:

  • 父组件创建完成
  • ImageCarousel组件被创建,并分配给carousel(通过ViewChild)
  • 我们不能访问carousel,直到ngAfterViewInit()(它将是null)
  • 我们分配配置,设置style_groupBG = 'red'
  • 这将依次在宿主ImageCarousel组件上设置background: red
  • 这个组件是由你的父组件“拥有”的,所以当它检查更改时,它发现了carousel.style.background上的更改,并且不够聪明,不知道这不是一个问题,所以它抛出异常。

一种解决方案是在ImageCarousel内部引入另一个包装器div,并在其上设置背景颜色,但这样就无法获得使用HostBinding的一些好处(例如允许父对象控制对象的全部边界)。

在父组件中,更好的解决方案是在设置配置之后添加detectChanges()。

ngAfterViewInit()
{
this.carousel.carouselConfig = { ... };
this.cdr.detectChanges();
}

这看起来很明显,和其他答案很相似,但有细微的区别。

考虑这样一种情况,您直到开发后期才添加@HostBinding。突然你得到这个错误,它似乎没有任何意义。

调试技巧

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

在父类的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抛出错误的确切位置。

enter image description here

您还可以单击灰色链接来放入断点。

另一件需要注意的事情是,如果您在整个应用程序中都有类似的命名属性(例如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()。看到# EYZ2

有一些有趣的答案,但我似乎没有找到一个符合我的需求,最接近的是来自@chittrang-mishra,它只指一个特定的功能,而不是像我的应用程序中那样有几个切换。

我不想使用[hidden]来利用*ngIf甚至不是DOM的一部分,所以我找到了以下解决方案,这可能不是最好的,因为它抑制错误而不是纠正它,但在我的情况下,我知道最终结果是正确的,这似乎对我的应用程序是可以的。

我所做的是实现AfterViewChecked,添加constructor(private changeDetector : ChangeDetectorRef ) {},然后

ngAfterViewChecked(){
this.changeDetector.detectChanges();
}

我希望这能帮助到其他人,就像其他人帮助过我一样。

Angular会运行更改检测,当它发现传递给子组件的某些值被更改时,Angular会抛出以下错误:

# EYZ0 # EYZ1

为了纠正这个错误,我们可以使用AfterContentChecked生命周期钩子和

import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';


constructor(
private cdref: ChangeDetectorRef) { }


ngAfterContentChecked() {


this.cdref.detectChanges();


}

在我的例子中,我在LoadingService中有一个行为主题isLoading的异步属性

使用[隐藏]模型可以工作,但是* ngIf模型失败

    <h1 [hidden]="!(loaderService.isLoading | async)">
THIS WORKS FINE
(Loading Data)
</h1>


<h1 *ngIf="!(loaderService.isLoading | async)">
THIS THROWS ERROR
(Loading Data)
</h1>

解决方案…服务和rxjs…事件发射器和属性绑定都使用rxjs..你最好自己实现它,更多的控制,更容易调试。记住,事件发射器使用rxjs。简单地说,创建一个服务,并在一个可观察对象中,让每个组件订阅该观察对象,并根据需要传递新值或消耗值

我希望这对来到这里的人有帮助: 我们以以下方式在ngOnInit中调用服务,并使用变量displayMain来控制元素到DOM的挂载

component.ts

  displayMain: boolean;
ngOnInit() {
this.displayMain = false;
// Service Calls go here
// Service Call 1
// Service Call 2
// ...
this.displayMain = true;
}

和component.html

<div *ngIf="displayMain"> <!-- This is the Root Element -->
<!-- All the HTML Goes here -->
</div>

我得到这个错误,因为我在component.html中使用了一个没有在component.ts中声明的变量。一旦我删除了HTML中的部分,这个错误就消失了。

我得到这个错误,因为我在模式中调度redux动作,当时模式还没有打开。我正在调度动作的瞬间模态组件接收输入。所以我把setTimeout放在那里,以确保模式是打开的,然后动作被分派。

我用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();
}

我使用的是ng2-carouselamos (Angular 8和Bootstrap 4)

下面这些步骤解决了我的问题:

  1. 实现# EYZ0
  2. 添加# EYZ0
  3. 然后# EYZ0

尝试了上面建议的大部分解决方案。只有在这种情况下对我有用。我使用*ngIf切换角材质的不确定渐进式条基于api调用,它抛出ExpressionChangedAfterItHasBeenCheckedError

在上述组件中:

constructor(
private ngZone: NgZone,
private changeDetectorRef: ChangeDetectorRef,
) {}


ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.appService.appLoader$.subscribe(value => {
this.loading = value;
this.changeDetectorRef.detectChanges();
});
});
}

诀窍是使用ngzone绕过角分量的变化检测。

PS:不确定这是否是一个优雅的解决方案,但是使用AfterContentChecked和AfterViewChecked生命周期钩子必然会引起性能问题,因为你的应用程序会越来越大,因为它会被多次触发。

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

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

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

html是这样的:

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

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

我把数据流改为可观察模式,因为我的“真实”数据来自秋田犬(一个状态管理库)。这意味着我的html变成:

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

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

当值在同一个更改检测周期中更改多次时,将发生此错误。我在一个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。

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

这一次没有发生错误,因此这修复了错误消息。

来源:# EYZ0

如果您在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);
...
}


}

添加static: true@ViewChild装饰器

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