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 修复。
此错误表明您的应用程序中确实存在问题,因此抛出异常是有意义的。
在 devMode
中,变化检测在每次常规变化检测后添加一个额外的回合 运行 检查模型是否已更改。
如果模型在常规变化检测和附加变化检测之间发生了变化,这表明要么
- 变化检测本身引起了变化
- 一个方法或getter returns 每次被调用时不同的值
这两个都不好,因为不清楚如何进行,因为模型可能永远不会稳定。
如果 Angular 运行 更改检测直到模型稳定,它可能 运行 永远。
如果 Angular 不 运行 更改检测,则视图可能不会反映模型的当前状态。
另见 What is difference between production and development mode in Angular2?
更新
我强烈建议首先从 开始:正确思考在 constructor
中可以做什么,而在 ngOnChanges()
中应该做什么。
原创
这与其说是一个答案,不如说是一个旁注,但它可能会对某些人有所帮助。我在尝试使按钮的存在取决于表单的状态时偶然发现了这个问题:
<button *ngIf="form.pristine">Yo</button>
据我所知,此语法会导致根据条件在 DOM 中添加和删除按钮。这又导致 ExpressionChangedAfterItHasBeenCheckedError
.
我的解决方法(虽然我没有声称掌握了差异的全部含义),而是使用 display: none
:
<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>
我遇到了类似的问题。查看 lifecycle hooks documentation,我将 ngAfterViewInit
更改为 ngAfterContentInit
并且有效。
我在 Ionic3(使用 Angular 4 作为其技术堆栈的一部分)中遇到了这种错误。
对我来说,它是这样做的:
<ion-icon [name]="getFavIconName()"></ion-icon>
所以我试图根据屏幕运行的模式有条件地将 ion-icon 的类型从 pin
更改为 remove-circle
。
我想我必须添加一个 *ngIf
。
我遇到了同样的问题,因为组件中的一个数组中的值发生了变化。但是我没有检测值变化的变化,而是将组件变化检测策略更改为 onPush
(它将检测对象变化而不是值变化)。
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
selector: -
......
})
一旦我理解了 the Angular Lifecycle Hooks 以及它们与变化检测的关系,我就明白了很多。
我试图让 Angular 更新绑定到元素 *ngIf
的全局标志,并且我试图在 ngOnInit()
生命周期内更改该标志另一个组件的钩子。
根据文档,此方法在 Angular 已经检测到更改后调用:
Called once, after the first 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 - "ExpressionChangedAfterItHasBeenCheckedError when changing a component 'non model' value in afterViewInit" 并决定添加 ngModel
<input type="hidden" ngModel #clientName />
解决了我的问题,希望对大家有帮助。
按照以下步骤操作:
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="isProcessing()"
使用 *ngIf 时,它会在每次条件更改时通过添加或删除元素来物理更改 DOM。因此,如果条件在呈现给视图之前发生变化(这在 Angular 的世界中极有可能),则会抛出错误。请参阅开发模式和生产模式之间的解释 。
[hidden]="isProcessing()"
当使用 [hidden]
时,它不会在物理上改变 DOM
,而只是从视图中隐藏 element
,最有可能在后面使用 CSS
。该元素仍在 DOM 中,但根据条件值不可见。这就是为什么使用 [hidden]
.
时不会发生错误的原因
因此,变更检测背后的机制实际上以同步执行变更检测和验证摘要的方式运作。这意味着,如果我们异步更新属性,当验证循环为 运行 时,值将不会更新,我们也不会得到 ExpressionChanged...
错误。我们收到此错误的原因是,在验证过程中,Angular 看到的值与它在变更检测阶段记录的值不同。所以为了避免这种情况....
1) 使用 changeDetectorRef
2) 使用 setTimeOut。这将在另一个 VM 中将您的代码作为宏任务执行。 Angular 在验证过程中不会看到这些更改,您也不会收到该错误。
setTimeout(() => {
this.isLoading = true;
});
3) 如果你真的想在同一个 VM 上执行代码,请使用
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()
(它将为空)
- 我们分配配置,设置
style_groupBG = 'red'
- 这又会在主机 ImageCarousel 组件上设置
background: red
- 此组件是您的父组件 'owned',因此当它检查更改时,它会在
carousel.style.background
上找到更改并且不够聪明,无法知道这不是问题,所以它抛出异常。
一个解决方案是引入另一个包装器 div insider ImageCarousel 并在其上设置背景颜色,但是这样你就无法获得使用 HostBinding
的一些好处(例如允许父级控制对象的完整边界)。
更好的方案,在父组件中设置config后添加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');
在子/服务/定时器回调中:
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
),则需要注意另一件事,确保您调试的是您认为的那个 - 通过将其设置为模糊的颜色值。
当我添加 *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
有一些有趣的答案,但我似乎没有找到满足我需求的答案,最接近的答案来自@chittrang-mishra,它仅指一个特定功能,而不是我的应用程序中的多个开关。
我不想使用 [hidden]
来利用 *ngIf
甚至不属于 DOM 所以我找到了以下可能不是最好的解决方案所有因为它抑制了错误而不是纠正它,但在我知道最终结果是正确的情况下,这对我的应用程序来说似乎没问题。
我所做的是实施 AfterViewChecked
,添加 constructor(private changeDetector : ChangeDetectorRef ) {}
,然后添加
ngAfterViewChecked(){
this.changeDetector.detectChanges();
}
我希望这对其他人有所帮助,就像其他许多人帮助过我一样。
Angular 运行变更检测,当它发现传递给子组件的某些值已被更改时,Angular 抛出以下错误:
ExpressionChangedAfterItHasBeenCheckedError
click for more
为了纠正这个问题,我们可以使用 AfterContentChecked
生命周期挂钩和
import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';
constructor(
private cdref: ChangeDetectorRef) { }
ngAfterContentChecked() {
this.cdref.detectChanges();
}
在我的例子中,我在 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...事件发射器和 属性 绑定都使用 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)
采取这些步骤解决了我的问题:
- 实施
AfterViewChecked
- 添加
constructor(private changeDetector : ChangeDetectorRef ) {}
- 然后
ngAfterViewChecked(){ this.changeDetector.detectChanges(); }
致任何为此苦苦挣扎的人。这是正确调试此错误的方法:https://blog.angular-university.io/angular-debugging/
在我的例子中,我确实使用这个 [hidden] hack 而不是 *ngIf...
摆脱了这个错误
但是我提供的 link 使我能够找到 THE GUILTY *ngIf :)
尽情享受吧。
尝试了上面建议的大部分解决方案。在这种情况下,只有这对我有用。我正在使用 *ngIf 根据 api 调用切换 angular material 的不确定进度条,它正在抛出 ExpressionChangedAfterItHasBeenCheckedError
.
在有问题的组件中:
constructor(
private ngZone: NgZone,
private changeDetectorRef: ChangeDetectorRef,
) {}
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.appService.appLoader$.subscribe(value => {
this.loading = value;
this.changeDetectorRef.detectChanges();
});
});
}
诀窍是使用 ngzone 绕过 angular 组件的变更检测。
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中完全没问题。
然后 我将数据流更改为 Observable 模式,因为我的 'real' 数据来自 Akita(状态管理库)。这意味着我的 html 变成了:
*ngFor="let x of (myArray$ | async)?.splice(0, 10)"
其中 myArray$ 是 [was] 类型 Observable<MyData[]>
,模板中的这种数据操作是导致错误发生的原因。不要像这样使用 RxJS 对象。
当一个值在同一个变化检测周期内变化不止一次时,会发生此错误。我遇到了一个 TypeScript getter 的问题,它的 return 值变化非常频繁。要解决此问题,您可以限制一个值,使其在每个更改检测周期内只能更改一次,如下所示:
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 改变时,你知道你在下一个循环。如果循环已更改,则更新值,return 否则 return 与之前在此循环中 return 编辑的值相同。
这样可以确保每个循环return只有一个值。鉴于更改检测周期如此频繁,这对于频繁更新值非常有效。
为了生成 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
这次没有发生错误,因此修复了错误消息。
如果在 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);
...
}
}
请向我解释为什么我一直收到此错误: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 修复。
此错误表明您的应用程序中确实存在问题,因此抛出异常是有意义的。
在 devMode
中,变化检测在每次常规变化检测后添加一个额外的回合 运行 检查模型是否已更改。
如果模型在常规变化检测和附加变化检测之间发生了变化,这表明要么
- 变化检测本身引起了变化
- 一个方法或getter returns 每次被调用时不同的值
这两个都不好,因为不清楚如何进行,因为模型可能永远不会稳定。
如果 Angular 运行 更改检测直到模型稳定,它可能 运行 永远。 如果 Angular 不 运行 更改检测,则视图可能不会反映模型的当前状态。
另见 What is difference between production and development mode in Angular2?
更新
我强烈建议首先从 constructor
中可以做什么,而在 ngOnChanges()
中应该做什么。
原创
这与其说是一个答案,不如说是一个旁注,但它可能会对某些人有所帮助。我在尝试使按钮的存在取决于表单的状态时偶然发现了这个问题:
<button *ngIf="form.pristine">Yo</button>
据我所知,此语法会导致根据条件在 DOM 中添加和删除按钮。这又导致 ExpressionChangedAfterItHasBeenCheckedError
.
我的解决方法(虽然我没有声称掌握了差异的全部含义),而是使用 display: none
:
<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>
我遇到了类似的问题。查看 lifecycle hooks documentation,我将 ngAfterViewInit
更改为 ngAfterContentInit
并且有效。
我在 Ionic3(使用 Angular 4 作为其技术堆栈的一部分)中遇到了这种错误。
对我来说,它是这样做的:
<ion-icon [name]="getFavIconName()"></ion-icon>
所以我试图根据屏幕运行的模式有条件地将 ion-icon 的类型从 pin
更改为 remove-circle
。
我想我必须添加一个 *ngIf
。
我遇到了同样的问题,因为组件中的一个数组中的值发生了变化。但是我没有检测值变化的变化,而是将组件变化检测策略更改为 onPush
(它将检测对象变化而不是值变化)。
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
selector: -
......
})
一旦我理解了 the Angular Lifecycle Hooks 以及它们与变化检测的关系,我就明白了很多。
我试图让 Angular 更新绑定到元素 *ngIf
的全局标志,并且我试图在 ngOnInit()
生命周期内更改该标志另一个组件的钩子。
根据文档,此方法在 Angular 已经检测到更改后调用:
Called once, after the first 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 - "ExpressionChangedAfterItHasBeenCheckedError when changing a component 'non model' value in afterViewInit" 并决定添加 ngModel
<input type="hidden" ngModel #clientName />
解决了我的问题,希望对大家有帮助。
按照以下步骤操作:
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="isProcessing()"
使用 *ngIf 时,它会在每次条件更改时通过添加或删除元素来物理更改 DOM。因此,如果条件在呈现给视图之前发生变化(这在 Angular 的世界中极有可能),则会抛出错误。请参阅开发模式和生产模式之间的解释
[hidden]="isProcessing()"
当使用 [hidden]
时,它不会在物理上改变 DOM
,而只是从视图中隐藏 element
,最有可能在后面使用 CSS
。该元素仍在 DOM 中,但根据条件值不可见。这就是为什么使用 [hidden]
.
因此,变更检测背后的机制实际上以同步执行变更检测和验证摘要的方式运作。这意味着,如果我们异步更新属性,当验证循环为 运行 时,值将不会更新,我们也不会得到 ExpressionChanged...
错误。我们收到此错误的原因是,在验证过程中,Angular 看到的值与它在变更检测阶段记录的值不同。所以为了避免这种情况....
1) 使用 changeDetectorRef
2) 使用 setTimeOut。这将在另一个 VM 中将您的代码作为宏任务执行。 Angular 在验证过程中不会看到这些更改,您也不会收到该错误。
setTimeout(() => {
this.isLoading = true;
});
3) 如果你真的想在同一个 VM 上执行代码,请使用
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()
(它将为空) - 我们分配配置,设置
style_groupBG = 'red'
- 这又会在主机 ImageCarousel 组件上设置
background: red
- 此组件是您的父组件 'owned',因此当它检查更改时,它会在
carousel.style.background
上找到更改并且不够聪明,无法知道这不是问题,所以它抛出异常。
一个解决方案是引入另一个包装器 div insider ImageCarousel 并在其上设置背景颜色,但是这样你就无法获得使用 HostBinding
的一些好处(例如允许父级控制对象的完整边界)。
更好的方案,在父组件中设置config后添加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');
在子/服务/定时器回调中:
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
),则需要注意另一件事,确保您调试的是您认为的那个 - 通过将其设置为模糊的颜色值。
当我添加 *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
有一些有趣的答案,但我似乎没有找到满足我需求的答案,最接近的答案来自@chittrang-mishra,它仅指一个特定功能,而不是我的应用程序中的多个开关。
我不想使用 [hidden]
来利用 *ngIf
甚至不属于 DOM 所以我找到了以下可能不是最好的解决方案所有因为它抑制了错误而不是纠正它,但在我知道最终结果是正确的情况下,这对我的应用程序来说似乎没问题。
我所做的是实施 AfterViewChecked
,添加 constructor(private changeDetector : ChangeDetectorRef ) {}
,然后添加
ngAfterViewChecked(){
this.changeDetector.detectChanges();
}
我希望这对其他人有所帮助,就像其他许多人帮助过我一样。
Angular 运行变更检测,当它发现传递给子组件的某些值已被更改时,Angular 抛出以下错误:
ExpressionChangedAfterItHasBeenCheckedError
click for more
为了纠正这个问题,我们可以使用 AfterContentChecked
生命周期挂钩和
import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';
constructor(
private cdref: ChangeDetectorRef) { }
ngAfterContentChecked() {
this.cdref.detectChanges();
}
在我的例子中,我在 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...事件发射器和 属性 绑定都使用 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)
采取这些步骤解决了我的问题:
- 实施
AfterViewChecked
- 添加
constructor(private changeDetector : ChangeDetectorRef ) {}
- 然后
ngAfterViewChecked(){ this.changeDetector.detectChanges(); }
致任何为此苦苦挣扎的人。这是正确调试此错误的方法:https://blog.angular-university.io/angular-debugging/
在我的例子中,我确实使用这个 [hidden] hack 而不是 *ngIf...
摆脱了这个错误但是我提供的 link 使我能够找到 THE GUILTY *ngIf :)
尽情享受吧。
尝试了上面建议的大部分解决方案。在这种情况下,只有这对我有用。我正在使用 *ngIf 根据 api 调用切换 angular material 的不确定进度条,它正在抛出 ExpressionChangedAfterItHasBeenCheckedError
.
在有问题的组件中:
constructor(
private ngZone: NgZone,
private changeDetectorRef: ChangeDetectorRef,
) {}
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.appService.appLoader$.subscribe(value => {
this.loading = value;
this.changeDetectorRef.detectChanges();
});
});
}
诀窍是使用 ngzone 绕过 angular 组件的变更检测。
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中完全没问题。
然后 我将数据流更改为 Observable 模式,因为我的 'real' 数据来自 Akita(状态管理库)。这意味着我的 html 变成了:
*ngFor="let x of (myArray$ | async)?.splice(0, 10)"
其中 myArray$ 是 [was] 类型 Observable<MyData[]>
,模板中的这种数据操作是导致错误发生的原因。不要像这样使用 RxJS 对象。
当一个值在同一个变化检测周期内变化不止一次时,会发生此错误。我遇到了一个 TypeScript getter 的问题,它的 return 值变化非常频繁。要解决此问题,您可以限制一个值,使其在每个更改检测周期内只能更改一次,如下所示:
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 改变时,你知道你在下一个循环。如果循环已更改,则更新值,return 否则 return 与之前在此循环中 return 编辑的值相同。
这样可以确保每个循环return只有一个值。鉴于更改检测周期如此频繁,这对于频繁更新值非常有效。
为了生成 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
这次没有发生错误,因此修复了错误消息。
如果在 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);
...
}
}