繁体   English   中英

ExpressionChangedAfterItHasBeenCheckedError 解释

[英]ExpressionChangedAfterItHasBeenCheckedError Explained

请向我解释为什么我不断收到此错误: 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 修复。

我有一个类似的问题。 查看生命周期挂钩文档,我将ngAfterViewInit更改为ngAfterContentInit并且它有效。

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

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

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

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

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

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

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

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

}

它解决了问题:)

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

ExpressionChangedAfterItHasBeenCheckedError 点击了解更多

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

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

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }

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

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

  1. 实施AfterViewChecked
  2. 添加constructor(private changeDetector : ChangeDetectorRef ) {}
  3. 然后ngAfterViewChecked(){ this.changeDetector.detectChanges(); } ngAfterViewChecked(){ this.changeDetector.detectChanges(); }

更新

我强烈建议首先从OP 的自我响应开始:正确考虑在constructor中可以做什么与在ngOnChanges()中应该做什么。

原来的

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

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

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

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

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

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

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

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

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

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

请按照以下步骤操作:

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

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

我不得不将ngIf更改[hidden]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

*ngIf="isProcessing()" 

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

[hidden]="isProcessing()"

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

调试提示

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

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

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

}

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

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

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

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

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

我猜我必须添加一个*ngIf来代替。

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

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

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

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

就我而言,我在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>

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

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

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

享受。

我在 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$ 是Observable<MyData[]>的 [was] 类型,模板中的这种数据操作是导致错误发生的原因。 不要对 RxJS 对象这样做。

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/

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

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

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

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

不管我得到多少赞成票,让我告诉你一个小故事。 该警告试图帮助您正确编程,但是如果您订阅生命周期内的可观察变量,请继续尝试解决此问题,但不要太着急。 如果您不能解决问题,请按原样保留代码,然后继续进行工作。

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

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

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

检查您的代码,有时当您在 lambda 过滤器或 find 表达式中使用“=”而不是“===”时会出现它。 我刚刚在我的一个服务模块中遇到了这个。

let myVar = this.arrayOfObjects.find(item => item.id = objectId);

以下是正确的:

let myVar = this.arrayOfObjects.find(item => item.id === objectId);

static: true添加到@ViewChild装饰器

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

ExpressionChangedAfterItHasBeenCheckedError 错误是在进行角度开发时可能发生的运行时错误。 它表示在检查组件视图后检测到组件模板的更改,这可能导致意外行为。

当正在检查组件的视图时,通常会发生此错误,并且对组件的模板或数据绑定进行更改会导致重新检查视图。 如果你想知道幕后发生了什么......这不是错误,这是一个功能错误在本文中完全逆向工程

https://blog.simplified.courses/angular-expression-changed-after-it-has-been-checked-error/

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM