请向我解释为什么我不断收到此错误:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
显然,我只在开发模式下获得它,它不会发生在我的生产版本中,但这很烦人,我根本不明白在我的开发环境中出现错误而不会出现在产品上的好处 - -可能是因为我缺乏了解。
通常,修复很容易,我只是将导致错误的代码包装在 setTimeout 中,如下所示:
setTimeout(()=> {
this.isLoading = true;
}, 0);
或者使用如下构造函数强制检测更改:constructor(private cd: ChangeDetectorRef) {}
:
this.isLoading = true;
this.cd.detectChanges();
但是为什么我经常遇到这个错误?我想了解它,以便将来避免这些骇人听闻的修复。
此错误表明您的应用程序中存在真正的问题,因此引发异常是有意义的。
在 devMode
中,更改检测会在每次常规更改检测运行后添加一个额外的轮次,以检查模型是否已更改。
如果模型在常规和附加变化检测轮次之间发生了变化,这表明要么
变化检测本身已经引起了变化
每次调用方法或 getter 时都会返回不同的值
这两者都很糟糕,因为模型可能永远不会稳定,因此不清楚如何进行。
如果 Angular 在模型稳定之前运行变化检测,它可能会一直运行。如果 Angular 不运行变更检测,那么视图可能不会反映模型的当前状态。
另请参阅What is difference between production and development mode in Angular2?
ngOnInit
或 ngOnChanges
之类的生命周期回调来修改模型引起的(一些生命周期回调允许修改其他人不允许修改的模型,我不记得自己究竟是哪个做或不做)。不要绑定到视图中的方法或函数,而是绑定到字段并更新事件处理程序中的字段。如果您必须绑定到方法,请确保它们始终返回相同的值实例,只要实际上没有更改。变更检测会大量调用这些方法。
changeRef.detectChanges()
是一种解决方案/抑制错误的事实证明了这一点。这就像在 Angular 1 中修改 $scope.$watch()
内的状态。
cdRef.detectChanges()
只在一些奇怪的边缘情况下是必需的,当你需要它时你应该仔细查看,你理解正确的原因。
get ClockValue() { return DateTime.TimeAMPM(new Date()) }
绑定到 getter 返回时间为“HH:MM”,它最终会在检测运行时发生变化时跳闸,我该如何解决这个问题?
一旦我了解了 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() {
}
它解决了问题:)
router.navigate
)。此代码最初放置在我收到错误的 AfterViewInit
中,然后我按照您对构造函数的说法移动,但它不尊重片段。移至 ngOnInit
已解决 :) 谢谢!
setInterval()
中也有效。
Angular 运行更改检测,当它发现某些已传递给子组件的值已更改时,Angular 会抛出以下错误:
ExpressionChangedAfterItHasBeenCheckedError
click for more
为了纠正这个问题,我们可以使用 AfterContentChecked
生命周期钩子和
import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';
constructor(
private cdref: ChangeDetectorRef) { }
ngAfterContentChecked() {
this.cdref.detectChanges();
}
我正在使用 ng2-carouselamos(Angular 8 和 Bootstrap 4)
采取这些步骤解决了我的问题:
实现 AfterViewChecked 添加构造函数(private changeDetector : ChangeDetectorRef ) {} 然后 ngAfterViewChecked(){ this.changeDetector.detectChanges(); }
更新
我强烈建议先从 the OP's self response 开始:正确考虑在 constructor
中可以做什么与在 ngOnChanges()
中应该做什么。
原来的
这与其说是一个答案,不如说是一个旁注,但它可能会对某人有所帮助。在尝试使按钮的存在取决于表单的状态时,我偶然发现了这个问题:
<button *ngIf="form.pristine">Yo</button>
据我所知,这种语法会导致按钮根据条件从 DOM 中添加和删除。这又导致 ExpressionChangedAfterItHasBeenCheckedError
。
在我的情况下,解决方法(尽管我没有声称掌握差异的全部含义)是使用 display: none
代替:
<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>
[hidden]
而不是非常冗长的 [style.display]
部分。 :)
[hidden]
will not always have the same behavior 为 display: none
有一些有趣的答案,但我似乎没有找到一个符合我需求的答案,最接近的是来自@chittrang-mishra,它只指一个特定的功能,而不是像我的应用程序中的几个切换。
我不想使用 [hidden]
来利用 *ngIf
甚至不是 DOM 的一部分,所以我发现以下解决方案可能不是对所有人最好的,因为它抑制错误而不是纠正它,但在我知道最终结果是正确的情况,这对我的应用程序来说似乎没问题。
我所做的是实现 AfterViewChecked
,添加 constructor(private changeDetector : ChangeDetectorRef ) {}
,然后
ngAfterViewChecked(){
this.changeDetector.detectChanges();
}
我希望这对其他人有所帮助,因为许多其他人已经帮助了我。
ngAfterViewChecked
在组件生命周期中只调用一次。触发变更检测周期不会重新启动组件生命周期,因此不会再次调用 ngAfterViewChecked
,除非从头开始重新渲染组件。 (请注意,其他一些挂钩,例如 ngDoCheck
,将由额外的变更检测周期触发,因为它们会误导性地挂钩到变更检测周期而不是直接连接到组件生命周期。在 ngDoCheck
中使用 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>
*ngIf
更改 DOM,在页面中添加和删除元素,而 [hidden]
更改项目的可见性,但不将其从 DOM 中删除。
我面临着同样的问题,因为我的组件中的一个数组中的值正在发生变化。但我没有检测值变化的变化,而是将组件变化检测策略更改为 onPush
(它将检测对象变化而不是值变化)。
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
selector: -
......
})
因此,变更检测背后的机制实际上是以同步执行变更检测和验证摘要的方式工作的。这意味着,如果我们异步更新属性,则在验证循环运行时不会更新值,也不会出现 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() 之前我们无法访问轮播(它将为空)
我们分配配置,设置 style_groupBG = 'red'
这反过来在主机 ImageCarousel 组件上设置背景:红色
该组件由您的父组件“拥有”,因此当它检查更改时,它会在 carousel.style.background 上发现更改,并且不够聪明,无法知道这不是问题,因此会引发异常。
一种解决方案是引入另一个包装器 div 内部 ImageCarousel 并在其上设置背景颜色,但是您不会获得使用 HostBinding
的一些好处(例如允许父级控制对象的完整边界)。
更好的解决方案是在父组件中设置配置后添加detectChanges()。
ngAfterViewInit()
{
this.carousel.carouselConfig = { ... };
this.cdr.detectChanges();
}
这可能看起来很明显,并且与其他答案非常相似,但存在细微差别。
考虑在开发后期才添加 @HostBinding
的情况。突然你得到这个错误,它似乎没有任何意义。
这是我对正在发生的事情的想法。我还没有阅读文档,但我确信这是显示错误的部分原因。
*ngIf="isProcessing()"
使用 *ngIf 时,它会在每次条件更改时通过添加或删除元素来物理更改 DOM。因此,如果条件在呈现到视图之前发生变化(这在 Angular 的世界中很有可能),就会引发错误。请参阅开发模式和生产模式之间的说明 here。
[hidden]="isProcessing()"
使用 [hidden]
时,它不会实际更改 DOM
,而只是从视图中隐藏 element
,很可能在后面使用 CSS
。该元素仍然存在于 DOM 中,但根据条件的值不可见。这就是使用 [hidden]
时不会发生错误的原因。
isProcessing()
做同样的事情,您必须使用 !isProcessing()
作为 [hidden]
hidden
没有“在后面使用 CSS”,它是一个常规的 HTML 属性。 developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/…
调试提示
这个错误可能会非常令人困惑,并且很容易对它发生的确切时间做出错误的假设。我发现在受影响的组件中的适当位置添加大量这样的调试语句很有帮助。这有助于理解流程。
在这样的父 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 在什么时候抛出错误。
https://i.stack.imgur.com/JuhhE.png
您也可以单击灰色链接来放置断点。
如果您在整个应用程序中具有类似名称的属性(例如 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
使用 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();
}
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
在 DOM 更改时触发值更改时发生
delay
使错误消失。它的工作原理与 setTimeout
类似。
我在 Ionic3 中遇到了这种错误(它使用 Angular 4 作为其技术堆栈的一部分)。
对我来说,它是这样做的:
<ion-icon [name]="getFavIconName()"></ion-icon>
因此,我试图根据屏幕运行的模式有条件地将 ion-icon 的类型从 pin
更改为 remove-circle
。
我猜我将不得不添加一个 *ngIf
。
对于我的问题,我正在阅读 github -“ExpressionChangedAfterItHasBeenCheckedError 在 afterViewInit 中更改组件'非模型'值时”并决定添加 ngModel
<input type="hidden" ngModel #clientName />
它解决了我的问题,我希望它可以帮助某人。
ngModel
。您能否详细说明为什么这会有所帮助?
就我而言,我在 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>
我在 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 的方法。
将 static: true 添加到 @ViewChild 装饰器
@ViewChild(UploadComponent, { static: true }) uploadViewChild: UploadComponent;
解决方案...服务和 rxjs...事件发射器和属性绑定都使用 rxjs..您最好自己实现它,更多控制,更容易调试。请记住,事件发射器正在使用 rxjs。简单地说,创建一个服务并在一个 observable 中,让每个组件订阅 tha 观察者,并根据需要传递新值或 cosume 值
不定期副业成功案例分享
setTimeout
将该更改包装到二属性就可以了。谢谢!ngAfterContentChecked
在这里工作,而ngAfterContentInit
仍然抛出错误。