从 child 更新 parent 组件中的值导致 Angular 中的 ExpressionChangedAfterItHasBeenCheckedError
Updating value in parent component from child one causes ExpressionChangedAfterItHasBeenCheckedError in Angular
我有两个组件:ParentComponent > ChildComponent
和一个服务,例如TitleService
.
ParentComponent
看起来像这样:
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor (private titleService: TitleService) {
}
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title)
;
}
ngOnDestroy (): void {
if (this.titleSubscription) {
this.titleSubscription.unsubscribe();
}
}
}
ChildComponent
看起来像这样:
export class ChildComponent implements OnInit {
constructor (
private route: ActivatedRoute,
private titleService: TitleService
) {
}
ngOnInit (): void {
// Updating title.
this.titleService.setTitle(this.route.snapshot.data.title);
}
}
想法很简单:ParentController
在屏幕上显示标题。为了始终呈现实际标题,它订阅了 TitleService
并监听事件。更改标题时,事件发生并更新标题。
当 ChildComponent
加载时,它从路由器获取数据(动态解析)并告诉 TitleService
使用新值更新标题。
问题是这个解决方案导致了这个错误:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'.
看起来值在变化检测轮中更新了。
我是否需要 re-arrange 代码以获得更好的实现,或者我是否必须在某处启动另一轮更改检测?
我可以将 setTitle()
和 onTitleChange()
调用移至受人尊敬的构造函数,但我读到它被认为是一种不好的做法 "heavy-lifting"在构造函数逻辑中,除了初始化本地属性。
另外,标题应该是由child组件决定的,所以无法从中提取出这个逻辑。
更新
我已经实现了一个最小的例子来更好地展示这个问题。您可以在 the GitHub repository.
中找到它
经过彻底调查,问题仅在 ParentComponent
中使用 *ngIf="title"
时出现:
<p>Parent Component</p>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
<hr>
<app-child></app-child>
您可以尝试使用 ChangeDetectorRef
,这样 Angular 将收到有关更改的手动通知,并且不会引发错误。
在 ParentComponent
中更改 title
后调用 ChangeDetectorRef
的 detectChanges
方法。
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor(private titleService: TitleService, private changeDetector: ChangeDetectorRef) {
}
public ngOnInit() {
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title);
this.changeDetector.detectChanges();
}
public ngOnDestroy(): void {
if (this.titleSubscription
) {
this.titleSubscription.unsubscribe();
}
}
}
文章 Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError
error 详细解释了此行为。
您的问题有两种可能的解决方案:
1) 在ngIf
之前加上app-child
:
<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
2) 使用异步事件:
export class TitleService {
private titleChangeEmitter = new EventEmitter<string>(true);
^^^^^^--------------
After thorough investigation the problem only occurred when using
*ngIf="title"
您描述的问题不是 ngIf
特有的,可以通过实施自定义指令轻松重现,该指令依赖于 parent 在更改期间同步更新的输入 将该输入传递给指令后的检测:
@Directive({
selector: '[aDir]'
})
export class ADirective {
@Input() aDir;
------------
<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError
为什么会发生这种情况实际上需要很好地理解 Angular 特定于变化检测和 component/directive 表示的内部结构。您可以从这些文章入手:
- The mechanics of DOM updates in Angular
- Everything you need to know about change detection in Angular
- Angular’s $digest is reborn in the newer version of Angular
虽然不可能在此答案中详细解释所有内容,但这里是高级解释。在 digest cycle Angular 期间对 child 指令执行某些操作。其中一项操作是更新输入并在 child directives/components 上调用 ngOnInit
生命周期挂钩。重要的是这些操作是严格按顺序执行的:
- 更新输入
- 调用 ngOnInit
现在您具有以下层次结构:
parent-component
ng-if
child-component
和Angular执行上述操作时遵循此层次结构。因此,假设当前 Angular 检查 parent-component
:
- 更新
title
ng-if
上的输入绑定,将其设置为初始值 undefined
- 为
ng-if
呼叫 ngOnInit
。
child-component
不需要更新
- 为
child-component
调用 ngOnInti
,在 parent-component
上将标题更改为 Dynamic Title
因此,我们最终遇到这样一种情况,即 Angular 在 ng-if
上更新属性时传给了 title=undefined
,但是当变更检测完成时,我们在 title=Dynamic Title
=19=]。现在,Angular 运行第二个摘要以验证没有变化。但是,当它与上一个摘要中传递给 ng-if
的值与当前值进行比较时,它会检测到变化并抛出错误。
更改 ng-if
和 parent-component
模板中的 child-component
的顺序将导致 parent-component
上的 属性 之前更新的情况angular 更新 ng-if
.
的属性
在我的例子中,我改变了我的数据状态--这个答案需要你阅读AngularInDepth.com摘要周期的解释--在 html 级别 内,我所要做的就是像这样改变我处理数据的方式:
<div>{{event.subjects.pop()}}</div>
进入
<div>{{event.subjects[0]}}</div>
总结:而不是弹出——returns 并删除数组的最后一个元素从而改变我的数据的状态——我使用正常的方式获取我的 数据而不改变它的状态从而防止异常。
在某些情况下 'quick' 解决方案是使用 setTimeout()
。
您需要考虑所有注意事项(请参阅其他人提到的文章),但对于像设置标题这样的简单情况,这是最简单的方法。
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {
setTimeout(() => { this.title = title; }, 0);
});
}
如果您还不能完全理解变更检测的所有复杂性,请将此作为最后的手段类型的解决方案。然后回来用另一种方式修复它:)
我有两个组件:ParentComponent > ChildComponent
和一个服务,例如TitleService
.
ParentComponent
看起来像这样:
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor (private titleService: TitleService) {
}
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title)
;
}
ngOnDestroy (): void {
if (this.titleSubscription) {
this.titleSubscription.unsubscribe();
}
}
}
ChildComponent
看起来像这样:
export class ChildComponent implements OnInit {
constructor (
private route: ActivatedRoute,
private titleService: TitleService
) {
}
ngOnInit (): void {
// Updating title.
this.titleService.setTitle(this.route.snapshot.data.title);
}
}
想法很简单:ParentController
在屏幕上显示标题。为了始终呈现实际标题,它订阅了 TitleService
并监听事件。更改标题时,事件发生并更新标题。
当 ChildComponent
加载时,它从路由器获取数据(动态解析)并告诉 TitleService
使用新值更新标题。
问题是这个解决方案导致了这个错误:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'.
看起来值在变化检测轮中更新了。
我是否需要 re-arrange 代码以获得更好的实现,或者我是否必须在某处启动另一轮更改检测?
我可以将
setTitle()
和onTitleChange()
调用移至受人尊敬的构造函数,但我读到它被认为是一种不好的做法 "heavy-lifting"在构造函数逻辑中,除了初始化本地属性。另外,标题应该是由child组件决定的,所以无法从中提取出这个逻辑。
更新
我已经实现了一个最小的例子来更好地展示这个问题。您可以在 the GitHub repository.
中找到它经过彻底调查,问题仅在 ParentComponent
中使用 *ngIf="title"
时出现:
<p>Parent Component</p>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
<hr>
<app-child></app-child>
您可以尝试使用 ChangeDetectorRef
,这样 Angular 将收到有关更改的手动通知,并且不会引发错误。
在 ParentComponent
中更改 title
后调用 ChangeDetectorRef
的 detectChanges
方法。
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor(private titleService: TitleService, private changeDetector: ChangeDetectorRef) {
}
public ngOnInit() {
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title);
this.changeDetector.detectChanges();
}
public ngOnDestroy(): void {
if (this.titleSubscription
) {
this.titleSubscription.unsubscribe();
}
}
}
文章 Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError
error 详细解释了此行为。
您的问题有两种可能的解决方案:
1) 在ngIf
之前加上app-child
:
<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
2) 使用异步事件:
export class TitleService {
private titleChangeEmitter = new EventEmitter<string>(true);
^^^^^^--------------
After thorough investigation the problem only occurred when using *ngIf="title"
您描述的问题不是 ngIf
特有的,可以通过实施自定义指令轻松重现,该指令依赖于 parent 在更改期间同步更新的输入 将该输入传递给指令后的检测:
@Directive({
selector: '[aDir]'
})
export class ADirective {
@Input() aDir;
------------
<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError
为什么会发生这种情况实际上需要很好地理解 Angular 特定于变化检测和 component/directive 表示的内部结构。您可以从这些文章入手:
- The mechanics of DOM updates in Angular
- Everything you need to know about change detection in Angular
- Angular’s $digest is reborn in the newer version of Angular
虽然不可能在此答案中详细解释所有内容,但这里是高级解释。在 digest cycle Angular 期间对 child 指令执行某些操作。其中一项操作是更新输入并在 child directives/components 上调用 ngOnInit
生命周期挂钩。重要的是这些操作是严格按顺序执行的:
- 更新输入
- 调用 ngOnInit
现在您具有以下层次结构:
parent-component
ng-if
child-component
和Angular执行上述操作时遵循此层次结构。因此,假设当前 Angular 检查 parent-component
:
- 更新
title
ng-if
上的输入绑定,将其设置为初始值undefined
- 为
ng-if
呼叫ngOnInit
。 child-component
不需要更新
- 为
child-component
调用ngOnInti
,在parent-component
上将标题更改为
Dynamic Title
因此,我们最终遇到这样一种情况,即 Angular 在 ng-if
上更新属性时传给了 title=undefined
,但是当变更检测完成时,我们在 title=Dynamic Title
=19=]。现在,Angular 运行第二个摘要以验证没有变化。但是,当它与上一个摘要中传递给 ng-if
的值与当前值进行比较时,它会检测到变化并抛出错误。
更改 ng-if
和 parent-component
模板中的 child-component
的顺序将导致 parent-component
上的 属性 之前更新的情况angular 更新 ng-if
.
在我的例子中,我改变了我的数据状态--这个答案需要你阅读AngularInDepth.com摘要周期的解释--在 html 级别 内,我所要做的就是像这样改变我处理数据的方式:
<div>{{event.subjects.pop()}}</div>
进入
<div>{{event.subjects[0]}}</div>
总结:而不是弹出——returns 并删除数组的最后一个元素从而改变我的数据的状态——我使用正常的方式获取我的 数据而不改变它的状态从而防止异常。
在某些情况下 'quick' 解决方案是使用 setTimeout()
。
您需要考虑所有注意事项(请参阅其他人提到的文章),但对于像设置标题这样的简单情况,这是最简单的方法。
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {
setTimeout(() => { this.title = title; }, 0);
});
}
如果您还不能完全理解变更检测的所有复杂性,请将此作为最后的手段类型的解决方案。然后回来用另一种方式修复它:)