从子1更新父组件中的值会导致Angular中的ExpressionChangedAfterItHasBeenCheckedError

时间:2017-06-20 14:23:54

标签: angular angular2-changedetection

我有两个组件: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'.

看起来该值在变更检测回合中更新。

我是否需要重新安排代码以实现更好的实施,还是我必须在某处启动另一个更改检测?

  • 我可以将setTitle()onTitleChange()来电转移给受人尊敬的施工人员,但我已经读到它被视为做任何事情的不良做法&#34 ;重升降"在构造函数逻辑中,除了初始化本地属性外。

  • 此外,标题应由子组件确定,因此无法从中提取该逻辑。

更新

我已经实施了一个最小的例子来更好地展示这个问题。您可以在the GitHub repository中找到它。

经过彻底调查后,只有在*ngIf="title"中使用ParentComponent时才会出现问题:

<p>Parent Component</p>

<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>

<hr>

<app-child></app-child>

4 个答案:

答案 0 :(得分:12)

文章Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error详细解释了这种行为。

您的问题有两种可能的解决方案:

1)将app-child放在ngIf之前:

<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>

2)使用异步事件:

export class TitleService {
  private titleChangeEmitter = new EventEmitter<string>(true);
                                                       ^^^^^^--------------
  

经过彻底调查后,问题只发生在使用时   * ngIf =&#34;标题&#34;

您所描述的问题并非特定于ngIf,并且可以通过实施自定义指令轻松复制,该指令取决于在输入传递后的更改检测期间同步更新的父输入到指令

@Directive({
  selector: '[aDir]'
})
export class ADirective {
  @Input() aDir;

------------

<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError

为什么发生这种情况实际上需要很好地理解特定于更改检测和组件/指令表示的Angular内部。你可以从这些文章开始:

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

  1. 更新输入
  2. 致电ngOnInit
  3. 现在您拥有以下层次结构:

    parent-component
        ng-if
        child-component
    

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

    1. title上更新ng-if输入绑定,将其设置为初始值undefined
    2. 致电ngOnInit ng-if
    3. child-component
    4. 无需更新
    5. 致电ngOnInti child-component,将标题更改为Dynamic Title上的parent-component
    6. 因此,在更新title=undefined上的属性时,我们最终会出现Angular传递ng-if的情况,但是当更改检测完成时,我们title=Dynamic Title上有parent-component。现在,Angular运行第二个摘要以验证没有变化。但是当它与前一个摘要传递给ng-if时使用当前值进行比较时,它会检测到更改并引发错误。

      更改ng-if模板中child-componentparent-component的顺序将导致parent-component上的属性更新ng-if之前的状态{ {1}}。

答案 1 :(得分:2)

您可以尝试使用ChangeDetectorRef,以便手动通知Angular有关更改,并且不会抛出错误。
title中更改ParentComponent后,请调用detectChanges的{​​{1}}方法。

ChangeDetectorRef

答案 2 :(得分:0)

在我的情况下,我更改了我的数据状态 - 这个答案要求您阅读AngularInDepth.com摘要周期的解释 - 在html级别内我所要做的就是改变我处理数据的方式:

<div>{{event.subjects.pop()}}</div>

<div>{{event.subjects[0]}}</div>

总结:而不是弹出 - 返回并删除数组的最后一个元素,从而改变我的数据状态 - 我使用了正常的方式获取数据而不改变其状态因此防止例外。

答案 3 :(得分:0)

在某些情况下,“快速”解决方案是使用setTimeout()。 您需要考虑所有注意事项(请参阅其他人提到的文章),但是对于简单的情况(例如仅设置标题),这是最简单的方法。

ngOnInit (): void {

  // Watching for title change.
  this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {

    setTimeout(() => { this.title = title; }, 0);

 });
}

如果您还不习惯了解变更检测的所有复杂性,请将其用作最后的解决方案。然后回来,并在其他时候用其他方式解决它:)