繁体   English   中英

从子1更新父组件中的值会导致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'.

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

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

  • 我可以将setTitle()onTitleChange()调用移动到受尊重的构造函数,但我已经读过,除了初始化本地属性之外,在构造函数逻辑中执行任何“繁重的工作”被认为是一种不好的做法。

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


更新

我已经实现了一个最小的例子来更好地演示这个问题。 您可以在GitHub存储库中找到它。

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

<p>Parent Component</p>

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

<hr>

<app-child></app-child>

文章关于ExpressionChangedAfterItHasBeenCheckedError错误需要了解的所有内容都会详细解释此行为。

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

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);
                                                       ^^^^^^--------------

彻底调查后,问题只发生在使用* ngIf =“title”时

您描述的问题并非特定于ngIf ,可以通过实现自定义指令轻松复制,该指令依赖于在输入传递给指令后在更改检测期间同步更新的父输入:

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

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

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

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

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

  1. 更新输入
  2. 调用ngOnInit

现在您有以下层次结构:

parent-component
    ng-if
    child-component

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

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

因此,我们最终得到的结果是Angular在更新ng-if上的属性时传递了title=undefined ,但是当更改检测完成时,我们在parent-component上有title=Dynamic Title 现在,Angular运行第二个摘要以验证没有变化。 但是当它与传递给ng-if内容相比时,它使用当前值来检测更改并抛出错误。

更改parent-component模板中ng-ifchild-component的顺序将导致parent-component属性在ng-if角度更新属性之前更新ng-if

您可以尝试使用ChangeDetectorRef以便手动通知Angular有关更改,并且不会抛出错误。
改变后titleParentComponent调用detectChanges的方法ChangeDetectorRef

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();
    }
  }

}

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

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

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

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

在某些情况下,“快速”解决方案是使用setTimeout() 你需要考虑所有的警告(参见其他人提到过的文章)但是对于一个简单的案例,比如设置一个标题,这是最简单的方法。

ngOnInit (): void {

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

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

 });
}

如果您还不能理解变更检测的所有复杂性,请将此作为最后的解决方案。 然后回来,当你是:)时,另一种方式修复它:)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM