2017-06-20 64 views
3

我有两个组件:ParentComponent > ChildComponent和一个服务,例如, TitleService更新父组件中父值的值会导致ExpressionChangedAfterItHasBeenCheckedError发生角度

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()调用推崇构造函数,但我读过,它被认为是一种不好的做法做任何“繁重工作”在构造逻辑,除了初始化局部性质。

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


更新

我已经实现了一个小例子,以便更好地说明这个问题。你可以在the GitHub repository找到它。

<p>Parent Component</p> 

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

<hr> 

<app-child></app-child> 
+0

您是否尝试使用ngAfterViewInit代替ParentComponent中的ngOnInit? –

+0

你可以显示你的路线配置吗? –

+0

还有组件模板? –

回答

8

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

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

1)把app-childngIf

<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并且可以通过实现依赖于母公司的输入之后输入传递到一个指令变化检测过程中被同步更新自定义指令很容易地复制:

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

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

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

为什么实际情况,需要有很好的理解角度内部特定的变化检测和组件/指令表示。你可以用这些文章开始:

虽然它不可能在这个答案来解释一切细节,这里是高层次的解释。在digest cycle期间,Angular对子指令执行某些操作。其中一项操作是更新输入并在子指令/组件上调用ngOnInit生命周期挂钩。最重要的是,这些操作都是在严格的顺序进行:

  1. 更新输入
  2. 呼叫ngOnInit

现在你有以下层次:

parent-component 
    ng-if 
    child-component 

和角以下层次结构当执行上述操作时。因此,假设目前角检查parent-component

  1. 更新title输入上ng-if结合,将其设置为初始值undefined
  2. 呼叫ngOnInitng-if
  3. 无需更新child-component
  4. 呼叫ngOnIntichild-component这改变标题Dynamic Titleparent-component

所以,我们最终在ng-if但是当变化更新属性时,其中角传下来title=undefined情况检测结束,我们有title=Dynamic Titleparent-component。现在,Angular运行第二个摘要来验证没有变化。但是,当它与前一个摘要中当前值ng-if相比较时,它会检测到更改并引发错误。

改变的ng-ifparent-component模板的顺序和child-component将导致局势当parent-component属性将之前的ng-if角度更新特性进行更新。

+0

谢谢你进行彻底的调查。它确实有帮助。我从来没有关于模板内部组件的实际顺序。这说得通。但是,更改模板中组件的顺序不是解决此问题的好方法。还有什么我可以做的吗?谢谢! –

+0

你可以异步设置标题,这将解决问题,[这里是类似的问题](https://stackoverflow.com/a/44691880/2545680) –

+0

使用'ServiceTitle'中的EventEmitter异步真正有助于缓解这个问题。再次感谢。你可以将这个解决方案添加到答案本身吗?我会很乐意接受它。这是我的头衔服务代码:https://gist.github.com/slavafomin/fd94cda8d63c1091e97e22ff6b7336cf –

1

你可以尝试使用ChangeDetectorRef所以角将手动通知有变化,误差不会抛出:

深入调查后在ParentComponent使用*ngIf="title"只时发生问题。
更改titleParentComponent致电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(); 
    } 
    } 

} 
+0

tnks,你的替代作品对我来说就像魔术一样! –

0

就我而言,我是改变我的数据 - 这答案需要你阅读AngularInDepth.com消化cycle-- 的HTML水平内部解释的状态下,所有我需要做的就是改变办法,我处理数据,象这样:

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

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

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

相关问题