从 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 代码以获得更好的实现,或者我是否必须在某处启动另一轮更改检测?


更新

我已经实现了一个最小的例子来更好地展示这个问题。您可以在 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 后调用 ChangeDetectorRefdetectChanges 方法。

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 表示的内部结构。您可以从这些文章入手:

虽然不可能在此答案中详细解释所有内容,但这里是高级解释。在 digest cycle Angular 期间对 child 指令执行某些操作。其中一项操作是更新输入并在 child directives/components 上调用 ngOnInit 生命周期挂钩。重要的是这些操作是严格按顺序执行的:

  1. 更新输入
  2. 调用 ngOnInit

现在您具有以下层次结构:

parent-component
    ng-if
    child-component

和Angular执行上述操作时遵循此层次结构。因此,假设当前 Angular 检查 parent-component:

  1. 更新 title ng-if 上的输入绑定,将其设置为初始值 undefined
  2. ng-if 呼叫 ngOnInit
  3. child-component
  4. 不需要更新
  5. child-component 调用 ngOnInti,在 parent-component
  6. 上将标题更改为 Dynamic Title

因此,我们最终遇到这样一种情况,即 Angular 在 ng-if 上更新属性时传给了 title=undefined,但是当变更检测完成时,我们在 title=Dynamic Title =19=]。现在,Angular 运行第二个摘要以验证没有变化。但是,当它与上一个摘要中传递给 ng-if 的值与当前值进行比较时,它会检测到变化并抛出错误。

更改 ng-ifparent-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);

 });
}

如果您还不能完全理解变更检测的所有复杂性,请将此作为最后的手段类型的解决方案。然后回来用另一种方式修复它:)