477

请向我解释为什么我不断收到此错误:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

显然,我只在开发模式下得到它,它不会发生在我的生产版本中,但这很烦人,我根本不明白在我的开发环境中出现错误而不会出现在产品上的好处 - -可能是因为我缺乏了解。

通常,修复很容易,我只是将导致错误的代码包装在 setTimeout 中,如下所示:

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

或者使用这样的构造函数强制检测更改constructor(private cd: ChangeDetectorRef) {}

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

但是为什么我经常遇到这个错误?我想了解它,以便将来避免这些骇人听闻的修复。

4

30 回答 30

184

我有一个类似的问题。查看生命周期钩子文档,我更改ngAfterViewInitngAfterContentInit并且它有效。

于 2017-07-27T03:57:42.400 回答
142

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

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

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

  • 变化检测本身已经引起了变化
  • 每次调用方法或 getter 时都会返回不同的值

这两者都很糟糕,因为模型可能永远不会稳定,因此不清楚如何进行。

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

另请参阅Angular2中的生产模式和开发模式有什么区别?

于 2017-04-12T17:03:46.103 回答
134

一旦我了解了 Angular Lifecycle Hooks及其与变更检测的关系,就会产生很多理解。

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

根据文档,在 Angular 已经检测到更改后调用此方法:

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

因此更新内部的标志ngOnChanges()不会启动更改检测。然后,一旦更改检测再次自然触发,标志的值就会发生更改并引发错误。

就我而言,我改变了这个:

constructor(private globalEventsService: GlobalEventsService) {

}

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

对此:

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

ngOnInit() {

}

它解决了问题:)

于 2018-01-11T21:52:18.107 回答
101

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

ExpressionChangedAfterItHasBeenCheckedError 点击查看更多

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

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

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }
于 2018-11-08T14:43:50.533 回答
71

我正在使用ng2-carouselamos(Angular 8 和 Bootstrap 4)

采取这些步骤解决了我的问题:

  1. 实施AfterViewChecked
  2. 添加constructor(private changeDetector : ChangeDetectorRef ) {}
  3. 然后ngAfterViewChecked(){ this.changeDetector.detectChanges(); }
于 2019-12-11T17:56:41.473 回答
46

更新

我强烈建议首先从OP 的自我反应开始:正确考虑constructorngOnChanges().

原来的

这与其说是一个答案,不如说是一个旁注,但它可能会对某人有所帮助。在尝试使按钮的存在取决于表单的状态时,我偶然发现了这个问题:

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

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

在我的情况下,解决方法(尽管我没有声称掌握了差异的全部含义),display: none而是使用:

<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>
于 2017-06-01T07:06:46.437 回答
42

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

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

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

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

我希望这对其他人有所帮助,因为许多其他人已经帮助了我。

于 2018-10-18T01:17:11.623 回答
39

请按照以下步骤操作:

1. 通过从@angular/core 导入来使用'ChangeDetectorRef',如下所示:

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

2.在constructor()中实现如下:

constructor(   private cdRef : ChangeDetectorRef  ) {}

3. 将以下方法添加到您在单击按钮等事件上调用的函数中。所以它看起来像这样:

functionName() {   
    yourCode;  
    //add this line to get rid of the error  
    this.cdRef.detectChanges();     
}
于 2018-05-01T10:27:49.170 回答
28

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

我不得不ngIf 改为 [hidden]

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

<app-loading [hidden]="!isLoading"></app-loading>
于 2018-02-22T19:52:06.780 回答
25

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

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

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush
    selector: -
    ......
})
于 2018-01-09T07:39:30.117 回答
22

参考文章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)如果您真的想在同一个VM上执行您的代码,请使用

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

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

于 2018-06-12T21:55:13.350 回答
12

尝试了上面建议的大多数解决方案。在这种情况下,只有这对我有用。我正在使用 *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 生命周期钩子必然会引发性能问题,因为您的应用程序会因为它被多次触发而变得更大。

于 2020-06-24T21:43:05.007 回答
10

@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()(它将为空)
  • 我们分配配置,它设置style_groupBG = 'red'
  • 这反过来又设置background: red在主机 ImageCarousel 组件上
  • 该组件由您的父组件“拥有”,因此当它检查更改时,它会发现更改carousel.style.background并且不够聪明,无法知道这不是问题,因此会引发异常。

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

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

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

这可能看起来很明显,并且与其他答案非常相似,但存在细微差别。

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

于 2018-08-02T21:01:21.133 回答
6

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

*ngIf="isProcessing()" 

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

[hidden]="isProcessing()"

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

于 2018-05-07T03:55:25.497 回答
5

调试提示

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

在这样的父 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');

在子/服务/计时器回调中:

    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),要注意的另一件事是确保您正在调试您认为的属性 - 通过将其设置为模糊的颜色值。

于 2018-08-02T21:19:58.220 回答
4

当我添加时,我的问题很明显,*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

于 2018-09-11T18:50:50.303 回答
2

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

对我来说,它是这样做的:

<ion-icon [name]="getFavIconName()"></ion-icon>

所以我试图有条件地将离子图标的类型从 apin更改为 a remove-circle,每个屏幕正在运行的模式。

我猜我将不得不添加一个*ngIf

于 2017-10-18T17:58:08.143 回答
2

使用 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();
}
于 2019-08-21T10:06:18.160 回答
1

对于我的问题,我正在阅读github - “ExpressionChangedAfterItHasBeenCheckedError when changed a component 'non model' value in afterViewInit”并决定添加 ngModel

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

它解决了我的问题,我希望它可以帮助某人。

于 2018-04-13T14:57:31.520 回答
1

LoadingService就我而言,我有一个带有 BehavioralSubject的异步属性isLoading

使用[hidden]模型有效,但*ngIf失败

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

    <h1 *ngIf="!(loaderService.isLoading | async)">
        THIS THROWS ERROR
        (Loading Data)
    </h1>
于 2019-04-25T00:47:39.410 回答
1

对于任何为此苦苦挣扎的人。这是正确调试此错误的一种方法:https ://blog.angular-university.io/angular-debugging/

就我而言,确实我使用这个 [hidden] hack 而不是 *ngIf... 摆脱了这个错误。

但是我提供的链接使我能够找到有罪的 * ngIf :)

享受。

于 2020-02-28T16:02:25.357 回答
0

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

组件.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>
于 2019-06-18T13:24:54.987 回答
0

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

于 2019-06-24T07:58:35.007 回答
0

我收到此错误是因为我在模态中调度 redux 操作,而当时没有打开模态。在模态组件收到输入的那一刻,我正在调度动作。所以我把 setTimeout 放在那里,以确保打开模式,然后分配动作。

于 2019-07-18T07:18:08.317 回答
0

我的问题是我在加载这个检查后正在更改的对象时打开了一个 Ngbmodal 弹出窗口。我能够通过打开 setTimeout 内的模式弹出窗口来解决它。

setTimeout(() => {
  this.modalReference = this.modalService.open(this.modal, { size: "lg" });
});
于 2021-03-04T15:49:47.517 回答
0

我在 RxJS/Observables 和静态模拟数据之间遇到了这个问题。起初,我的应用程序使用静态模拟数据,在我的例子中是数据数组

html是这样的:

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

所以这个想法是只显示最多 10 个元素myArraysplice()获取原始数组的副本。据我所知,这在 Angular 中非常好

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

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

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

于 2021-04-20T08:59:56.600 回答
0

当一个值在同一个变化检测周期中变化不止一次时,会发生此错误。我遇到了一个 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 的方法。

于 2021-10-12T14:10:37.127 回答
0

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/

于 2021-11-23T11:57:21.070 回答
0

如果您在触发 inside 时看到,ExpressionChangedAfterItHasBeenCheckedError那么您可以在创建时传递以使其在当前更改检测周期后异步发出。EventEmitterngAfterViewInit()trueEventEmitter

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

  ...

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

}
于 2022-01-27T15:57:13.530 回答
-1

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

于 2019-06-05T14:47:35.793 回答