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

Sla*_* II 9 angular2-changedetection 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();
    }
  }    

}
Run Code Online (Sandbox Code Playgroud)

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

  }

}
Run Code Online (Sandbox Code Playgroud)

这个想法非常简单:ParentController在屏幕上显示标题.为了始终呈现实际标题,它订阅TitleService并监听事件.更改标题后,将发生事件并更新标题.

ChildComponent负载,它从路由器中获取数据(这是动态解析),并告诉TitleService更新与新价值的称号.

问题是此解决方案导致此错误:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'.

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

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

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

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


更新

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

经过彻底调查后,问题只发生在使用*ngIf="title"ParentComponent:

<p>Parent Component</p>

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

<hr>

<app-child></app-child>
Run Code Online (Sandbox Code Playgroud)

Max*_*kyi 13

文章您需要了解的有关ExpressionChangedAfterItHasBeenCheckedError错误的所有内容都会详细说明此行为.

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

1)放在app-child前面ngIf:

<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
Run Code Online (Sandbox Code Playgroud)

2)使用异步事件:

export class TitleService {
  private titleChangeEmitter = new EventEmitter<string>(true);
                                                       ^^^^^^--------------
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 更新输入
  2. 调用ngOnInit

现在您有以下层次结构:

parent-component
    ng-if
    child-component
Run Code Online (Sandbox Code Playgroud)

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

  1. 更新title输入绑定ng-if,将其设置为初始值undefined
  2. 打电话ngOnInitng-if.
  3. 无需更新 child-component
  4. 打电话ngOnIntichild-component这改变标题Dynamic Titleparent-component

因此,我们最终会遇到Angular title=undefined在更新属性时传递的情况,ng-if但是当更改检测完成时我们已经title=Dynamic Title开启了parent-component.现在,Angular运行第二个摘要以验证没有变化.但是当它与ng-if前一个摘要传递给当前值的内容相比时,它会检测到更改并引发错误.

更改的顺序ng-ifchild-componentparent-component模板将导致局势时,物业parent-component将前一个角度的更新特性进行更新ng-if.


Uns*_*Sam 6

您可以尝试使用ChangeDetectorRefAngular 将手动通知有关更改的通知,并且不会抛出错误。
改变后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();
    }
  }

}
Run Code Online (Sandbox Code Playgroud)