ExpressionChangedAfterItHasBeenCheckedError解释

时间:2017-04-12 16:59:34

标签: angular angular2-changedetection angular2-databinding

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

显然,我只是在开发模式下获得它,它不会在我的生产版本中发生,但它非常烦人,而我根本不理解在我的开发环境中出现错误的好处不会显示刺激 - 可能是因为我缺乏理解。

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

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

或使用如下构造函数强制检测更改:constructor(private cd: ChangeDetectorRef) {}

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

但为什么我经常遇到这个错误?我想了解它,以便将来可以避免这些hacky修复。

29 个答案:

答案 0 :(得分:77)

我有类似的问题。查看lifecycle hooks documentation,我将ngAfterViewInit更改为ngAfterContentInit并且它有效。

答案 1 :(得分:70)

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

devMode变更检测中,在每次定期更改检测运行后添加额外的转弯以检查模型是否已更改。

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

  • 更改检测本身已导致更改
  • 每次调用方法或getter时都会返回一个不同的值

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

如果Angular运行更改检测,直到模型稳定,它可能会永远运行。 如果Angular没有运行变更检测,那么视图可能无法反映模型的当前状态。

另见What is difference between production and development mode in Angular2?

答案 2 :(得分:43)

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

}

它解决了问题:)

答案 3 :(得分:32)

<强>更新

我强烈建议首先从the OP's self response开始:正确考虑 private static void searchDocx(File file, String searchText) throws IOException { FileInputStream fis = new FileInputStream(file.getAbsolutePath()); XWPFDocument document = new XWPFDocument(fis); int pageNo = 1; for (XWPFParagraph paragraph : document.getParagraphs()) { String text = paragraph.getText(); if (text != null) { if (text.toLowerCase().contains(searchText.toLowerCase())) { System.out.println("found on page: " + pageNo+ " in: " + file.getAbsolutePath()); } } if (paragraph.isPageBreak()) { pageNo++; } } } constructor中应该做些什么相对应。

<强>原始

这是一个侧面说明而非答案,但它可能对某人有所帮助。当我试图使按钮的存在取决于表单的状态时,我偶然发现了这个问题:

ngOnChanges()

据我所知,这种语法会导致根据条件在DOM中添加和删除按钮。这又会导致<button *ngIf="form.pristine">Yo</button>

我的案例中的修复(虽然我没有声称要抓住差异的全部含义),但是改为使用ExpressionChangedAfterItHasBeenCheckedError

display: none

答案 4 :(得分:17)

在我的情况下,我在运行测试时在我的spec文件中遇到了这个问题。

我必须将ngIf 更改为 [hidden]

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

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

答案 5 :(得分:13)

有一些有趣的答案,但我似乎找不到一个能满足我需求的答案,最接近的是@ chittrang-mishra,它仅指一个特定功能,而不像我的应用程序中那样涉及多个切换。

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

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

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

我希望这能对其他人有所帮助。

答案 6 :(得分:13)

按照以下步骤操作:

1。 使用'ChangeDetectorRef'从@ angular / core导入它,如下所示:

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

2。 在构造函数()中实现它,如下所示:

constructor(   private cdRef : ChangeDetectorRef  ) {}

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

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

答案 7 :(得分:12)

我遇到了同样的问题,因为我的组件中的一个数组中的值发生了变化。但是,我没有检测到值变化的变化,而是将组件更改检测策略更改为<div class="input-group add-on"> <input class="form-control" placeholder="Search" name="srch-term" id="srch-term" type="text" onkeypress="autoFill()"/> <div class="input-group-btn"> <button class="btn btn-default" type="submit" onclick="searchStateName();"><i class="glyphicon glyphicon-search"></i></button> </div> </div> function searchStateName() { input = document.getElementById("srch-term").value.toUpperCase(); if (input) { for (var i = 0; i < countryData.length; i++) { //alert(countryData[0]); if (countryData[i].P.name_1.toUpperCase() == input) { alert(countryData[i].P.name_1); var extent = countryData[i].P.geom.getExtent(); map.getView().fit(extent, map.getSize()); if (countryData[i] !== highlight) { if (highlight) { featureOverlay.getSource().removeFeature(highlight); } if (countryData[i]) { featureOverlay.getSource().addFeature(countryData[i]); } highlight = countryData[i]; } } } } } (它将检测对象更改的更改,而不是值更改)。

onPush

答案 8 :(得分:10)

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

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

答案 9 :(得分:9)

Angular运行更改检测,当它发现传递给子组件的某些值已更改时,Angular会引发错误ExpressionChangedAfterItHasBeenCheckedError click for More

为了纠正这一点,我们可以使用AfterContentChecked生命周期挂钩和

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

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }

答案 10 :(得分:5)

尝试了以上建议的大多数解决方案。在这种情况下,只有这对我有用。我正在使用* ngIf来基于api调用切换角度材质的不确定的进度条,并且它抛出了lowEntery

在相关组件中:

else

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

PS:不确定这是否是一种优雅的解决方案,但是使用AfterContentChecked和AfterViewChecked生命周期挂钩势必会引发性能问题,因为您的应用程序会被多次触发而变得更大。

答案 11 :(得分:2)

@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'
  • 这反过来在主机ImageCarousel组件上设置background: red
  • 此组件由您的父组件“拥有”,因此,当它检查更改时,会在carousel.style.background上找到更改,并且不够聪明地知道这不是问题,因此会引发异常。

一种解决方案是引入另一个wrapper div内部ImageCarousel并在其上设置背景色,但是这样您将无法获得使用HostBinding的一些好处(例如,允许父级控制整个范围)的对象)。

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

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

这样的设置看起来很明显,与其他答案非常相似,但是有细微的差别。

请考虑在开发过程中直到以后才添加@HostBinding的情况。突然,您收到此错误,似乎没有任何意义。

答案 12 :(得分:1)

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

*ngIf="isProcessing()" 

使用* ngIf时,它会在每次条件更改时通过添加或删除元素来物理更改DOM。因此,如果条件在渲染到视图之前发生更改(这在Angular的世界中非常可能),则会抛出错误。请参阅开发和生产模式之间的解释here

[hidden]="isProcessing()"

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

答案 13 :(得分:1)

对于我的问题,我正在阅读github - &#34; ExpressionChangedAfterItHasBeenCheckedError更改组件&#39;非模型&#39; afterViewInit&#34;中的值并决定添加ngModel

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

它修复了我的问题,我希望它可以帮到某人。

答案 14 :(得分:1)

当我添加*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

答案 15 :(得分:1)

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

对我而言,这样做:

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

所以我试图根据屏幕运行的模式,有条件地将ion-icon的类型从pin更改为remove-circle

我猜我不得不添加* ngIF。

答案 16 :(得分:1)

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

答案 17 :(得分:0)

我在 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)"

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

答案 18 :(得分:0)

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

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

答案 19 :(得分:0)

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

就我而言,确实,我使用此[隐藏] hack代替了* ngIf ...,摆脱了这个错误。

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

享受。

答案 20 :(得分:0)

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

下面解决了我的问题,我所做的是实现AfterViewChecked,添加构造函数(private changeDetector:ChangeDetectorRef){}

然后

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

答案 21 :(得分:0)

我收到此错误,是因为我正在以模态分派redux动作,并且当时没有打开模态。我在模态组件接收到输入时立即分派动作。因此,我将setTimeout放在那里,以确保打开了模态,然后插入了动作。

答案 22 :(得分:0)

我在订阅范围中有一个代码块,我只是将其更改为上限范围,所以工作正常。

答案 23 :(得分:0)

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

答案 24 :(得分:0)

我希望这可以帮助到这里来的人: 我们以以下方式在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>

答案 25 :(得分:0)

解决方案...服务和rxjs ...事件发射器和属性绑定都使用rxjs ..您更好地实现了自己,更好的控制,更易于调试。请记住,事件发射器正在使用rxjs。简单地,创建一个服务并在可观察范围内,让每个组件订阅观察者并根据需要传递新值或假定值

答案 26 :(得分:0)

在我的情况下,我在LoadingService中具有一个BehavioralSubject isLoading的异步属性

使用 [隐藏] 模型有效,但是 * ngIf 失败

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

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

答案 27 :(得分:0)

调试技巧

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

在这样的父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抛出错误的时间。

enter image description here

您也可以单击灰色链接以放置断点。

要注意的是,如果您在整个应用程序中都具有类似名称的属性(例如style.background),则可以通过将其设置为模糊的颜色值来确保您正在调试自己认为的属性。

答案 28 :(得分:-7)

我在这个问题上挣扎了一段时间,主要是在我的开发环境中运行测试时。我能够通过围绕服务调用响应的简单setTimeout解决此问题!