简体   繁体   English

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

[英]Updating value in parent component from child one causes ExpressionChangedAfterItHasBeenCheckedError in Angular

I have two component: ParentComponent > ChildComponent and a service, eg TitleService . 我有两个组件: ParentComponent > ChildComponent和一个服务,例如TitleService

ParentComponent looks like this: 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 looks like this: 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);

  }

}

The idea is very simple: ParentController displays the title on screen. 这个想法非常简单: ParentController在屏幕上显示标题。 In order to always render the actual title it subscribes to the TitleService and listens for events. 为了始终呈现实际标题,它订阅TitleService并侦听事件。 When title is changed, the event happens and title is updated. 更改标题后,将发生事件并更新标题。

When ChildComponent loads, it gets data from the router (which is resolved dynamically) and tells TitleService to update the title with the new value. ChildComponent加载时,它从路由器获取数据(动态解析)并告诉TitleService使用新值更新标题。

The problem is this solution causes this error: 问题是此解决方案导致此错误:

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

It looks like the value is updated in a change detection round. 看起来该值在更改检测回合中更新。

Do I need to re-arrange the code to have a better implementation or do I have to initiate another change detection round somewhere? 我是否需要重新安排代码以实现更好的实现,还是我必须在某个地方启动另一个更改检测?

  • I can move the setTitle() and onTitleChange() calls to the respected constructors, but I've read that it's considered a bad practice to do any "heavy-lifting" in the constructor logic, besides initializing local properties. 我可以将setTitle()onTitleChange()调用移动到受尊重的构造函数,但我已经读过,除了初始化本地属性之外,在构造函数逻辑中执行任何“繁重的工作”被认为是一种不好的做法。

  • Also, the title should be determined by the child component, so this logic couldn't be extracted from it. 此外,标题应由子组件确定,因此无法从中提取此逻辑。


Update 更新

I've implemented a minimal example to better demonstrate the issue. 我已经实现了一个最小的例子来更好地演示这个问题。 You can find it in the GitHub repository . 您可以在GitHub存储库中找到它。

After thorough investigation the problem only occurred when using *ngIf="title" in ParentComponent : 彻底调查后,只有在ParentComponent使用*ngIf="title"时才会出现问题:

<p>Parent Component</p>

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

<hr>

<app-child></app-child>

The article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error explains this behavior in great details. 文章关于ExpressionChangedAfterItHasBeenCheckedError错误需要了解的所有内容都会详细解释此行为。

There are two possible solutions to your problem: 您的问题有两种可能的解决方案:

1) put app-child before ngIf : 1)在ngIf之前放置app-child

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

2) Use asynchronous event: 2)使用异步事件:

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

After thorough investigation the problem only occurred when using *ngIf="title" 彻底调查后,问题只发生在使用* ngIf =“title”时

The problem you're describing is not specific to ngIf and can be easily reproduced by implementing a custom directive that depends on parent input that is synchronously updated during change detection after that input was passed down to a directive : 您描述的问题并非特定于ngIf ,可以通过实现自定义指令轻松复制,该指令依赖于在输入传递给指令后在更改检测期间同步更新的父输入:

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

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

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

Why that happens actually requires a good understanding of Angular internals specific to change detection and component/directive representation. 为什么发生这种情况实际上需要很好地理解特定于更改检测和组件/指令表示的Angular内部。 You can start with these articles: 你可以从这些文章开始:

Although it's not possible to explain everything in details in this answer, here is the high level explanation. 虽然在这个答案中不可能详细解释所有内容,但这里有高级别的解释。 During digest cycle Angular performs certain operations on child directives. 摘要周期中, Angular对子指令执行某些操作。 One of such operations is updating inputs and calling ngOnInit lifecycle hook on child directives/components. 其中一个操作是更新输入并在子指令/组件上调用ngOnInit生命周期钩子。 What's important is that these operations are performed in strict order: 重要的是这些操作是按严格的顺序执行的:

  1. Update inputs 更新输入
  2. Call ngOnInit 调用ngOnInit

Now you have the following hierarchy: 现在您有以下层次结构:

parent-component
    ng-if
    child-component

And Angular follows this hierarchy when performing above operations. 在执行上述操作时,Angular遵循此层次结构。 So, assume currently Angular checks parent-component : 因此,假设当前Angular检查parent-component

  1. Update title input binding on ng-if , set it to initial value undefined 更新ng-if上的title输入绑定,将其设置为初始值undefined
  2. Call ngOnInit for ng-if . ng-if调用ngOnInit
  3. No update is required for child-component child-component不需要更新
  4. Call ngOnInti for child-component which changes title to Dynamic Title on parent-component child-component调用ngOnInti ,在parent-component child-component上更改Dynamic Title

So, we end up with a situation where Angular passed down title=undefined when updating properties on ng-if but when change detection is finished we have title=Dynamic Title on parent-component . 因此,我们最终得到的结果是Angular在更新ng-if上的属性时传递了title=undefined ,但是当更改检测完成时,我们在parent-component上有title=Dynamic Title Now, Angular runs second digest to verify there's no changes. 现在,Angular运行第二个摘要以验证没有变化。 But when it compares to what was passed down to ng-if on the previous digest with the current value it detects a change and throws an error. 但是当它与传递给ng-if内容相比时,它使用当前值来检测更改并抛出错误。

Changing the order of ng-if and a child-component in the parent-component template will lead to the situation when property on parent-component will be updated before angular updates properties for a ng-if . 更改parent-component模板中ng-ifchild-component的顺序将导致parent-component属性在ng-if角度更新属性之前更新ng-if

You could try using the ChangeDetectorRef so Angular will be manually notified about the change and the error won't be thrown. 您可以尝试使用ChangeDetectorRef以便手动通知Angular有关更改,并且不会抛出错误。
After changing title in ParentComponent call the detectChanges method of the ChangeDetectorRef . 改变后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();
    }
  }

}

In my case, I was changing the state of my data --this answer requires you to read AngularInDepth.com explanation of digest cycle-- inside the html level , all I had to do was change the way I was handling the data like so: 在我的情况下,我正在改变我的数据状态 - 这个答案要求你阅读AngularInDepth.com的摘要周期解释 - 在html级别内 ,我所要做的就是改变我处理数据的方式,如此:

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

into

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

Summary: instead of popping--which returns and remove the last element of an array thus changing the state of my data-- I used the normal way of getting my data without changing its state thus preventing the exception. 总结:而不是弹出 - 它返回并删除数组的最后一个元素,从而改变我的数据状态 - 我使用正常的方式获取我的数据而不改变其状态,从而防止异常。

A 'quick' solution in some cases is to use setTimeout() . 在某些情况下,“快速”解决方案是使用setTimeout() You need to consider all the caveats (see the articles other people have referred to) but for a simple case like just setting a title this is the easiest way. 你需要考虑所有的警告(参见其他人提到过的文章)但是对于一个简单的案例,比如设置一个标题,这是最简单的方法。

ngOnInit (): void {

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

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

 });
}

Use this as a last resort type solution if you're not yet comfortable understanding all the complexities of change detection. 如果您还不能理解变更检测的所有复杂性,请将此作为最后的解决方案。 Then come back and fix it another way when you are :) 然后回来,当你是:)时,另一种方式修复它:)

暂无
暂无

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

相关问题 从 Angular 中的子组件更新一个父数组值 - Updating one parent array value from a child component in Angular 子级初始化之后,子级DOM上父组件的操作导致ExpressionChangedAfterItHaHasBeenCheckedError - After child has been initialised, operation from parent component on child DOM causes ExpressionChangedAfterItHasBeenCheckedError 使用 Map 将数据从父组件传递到子组件时出现 Angular ExpressionChangedAfterItHasBeenCheckedError - Angular ExpressionChangedAfterItHasBeenCheckedError when passing data from Parent Component to Child Component using Map ExpressionChangedAfterItHasBeenCheckedError 在更新到 Angular 9 后,在启用 Ivy 时从子项更改父属性 - ExpressionChangedAfterItHasBeenCheckedError after updating to Angular 9 on changing parent property from the child when Ivy is enabled Angular子级更新父级和ExpressionChangedAfterItHaHasBeenCheckedError - Angular child update parent and ExpressionChangedAfterItHasBeenCheckedError 角度:ExpressionChangedAfterItHasBeenCheckedError子组件和FormControl - Angular: ExpressionChangedAfterItHasBeenCheckedError child component & FormControl Angular 2 [typescript]使用json模型从子组件UI更新父组件UI的模型值 - Angular 2 [ typescript] Updating a model value of parent component UI from a child component UI with json model 从父组件调用时,角子组件变量未更新 - Angular child component variable not updating when called from parent component 来自父组件的角度更新子组件输入 - Angular updating child component input from parent component 从子组件[Angular]访问父值 - Access to the parent value from child component [Angular]
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM