如何管理 Angular2 "expression has changed after it was checked"组件属性依赖于当前日期时间时的异常



我的组件的样式依赖于当前日期时间。在我的组件中,我有以下函数:

  private fontColor( dto : Dto ) : string {
    // date d'exécution du dto
    let dtoDate : Date = new Date( dto.LastExecution );
    (...)
    let color =  "hsl( " + hue + ", 80%, " + (maxLigness - lightnessAmp) + "%)";
    return color;
  }

lightnessAmp从当前日期时间计算。如果dtoDate在过去24小时内,颜色会改变。

准确的错误如下:

表达式在检查后发生了变化。前一个值:"hsl(123, 80%, 49%)"。当前值:'hsl(123, 80%, 48%)'

我知道异常只在检查值时出现在开发模式中。如果检查的值与更新的值不同,则抛出异常。

因此,我尝试在以下钩子方法中更新每个生命周期的当前日期时间,以防止异常:

  ngAfterViewChecked()
  {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
  }

…但是没有成功。

在更改后显式运行更改检测:

import { ChangeDetectorRef } from '@angular/core';
constructor(private cdRef:ChangeDetectorRef) {}
ngAfterViewChecked()
{
  console.log( "! changement de la date du composant !" );
  this.dateNow = new Date();
  this.cdRef.detectChanges();
}

TL;博士

ngAfterViewInit() {
    setTimeout(() => {
        this.dateNow = new Date();
    });
}

虽然这是一种变通方法,但有时真的很难用更好的方法解决这个问题,所以如果你使用这种方法,不要责怪自己。没关系。

示例:初始问题[link],用setTimeout() [link]解决


如何避免

在一般情况下,这个错误通常发生在你添加某处(甚至在父/子组件)ngAfterViewInit。所以第一个问题是问自己——没有ngAfterViewInit我能活下去吗?也许您可以将代码移到某个地方(ngAfterViewChecked可能是另一种选择)。

例子:[链接]


ngAfterViewInit中影响DOM的异步内容也可能导致此问题。也可以通过setTimeout或在管道中添加delay(0)操作符来解决:

ngAfterViewInit() {
  this.foo$
    .pipe(delay(0)) //"delay" here is an alternative to setTimeout()
    .subscribe();
}

例子:[链接]


好阅读

关于如何调试以及为什么会发生这种情况的好文章:link

有两个解决方案!


1。修改ChangeDetectionStrategyOnPush

对于这个解,你基本上告诉angular:

停止检查更改;只有当我知道有必要的时候,我才会这样做。

修改你的组件,让它像这样使用ChangeDetectionStrategy.OnPush:

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
    // ...
}

有了这个,事情似乎不再工作了。这是因为从现在开始,你必须让Angular手动调用detectChanges()

this.cdr.detectChanges();

如果你感兴趣,请查看这篇文章。它帮助我了解ChangeDetectionStrategy是如何工作的。


2。理解ExpressionChangedAfterItHasBeenCheckedError

请查看有关该问题的视频(太棒了!)另外,下面是本文中关于这个错误的原因的一小段摘录,我试图只包括有助于我理解这个错误的部分。

完整的文章展示了关于这里所示的每个点的真实代码示例。

根本原因是angular的生命周期:

每次操作之后,Angular都会记住它曾经执行过的值一个操作。类的oldValues属性中存储它们组件视图。

当所有组件的检查都完成后,Angular就会启动在下一个摘要循环中,它将当前值与它所记住的值进行比较,而不是执行操作前一个摘要循环。

在摘要循环中检查以下操作:

检查传递给子组件的值是否与将用于更新这些组件的属性的值现在。

检查用于更新DOM元素的值是否与将用于更新这些元素的值现在执行相同。

检查所有子组件

因此,当比较的值不同时,将抛出错误。,博主Max Koretskyi说:

罪魁祸首总是子组件或指令。

最后这里有一些现实世界的例子,通常会导致这个错误:

  • 共享服务(示例)
  • 同步事件广播(示例)
  • 动态组件实例化 (example)

在我的例子中,问题是一个动态组件实例化。

另外,根据我自己的经验,我强烈建议大家避免setTimeout解决方案,在我的情况下,导致了一个"几乎";无限循环(21个调用,我不愿意向您展示如何激怒它们),

我建议始终牢记Angular的生命周期,这样你就可以考虑每次修改另一个组件的值时它们会受到怎样的影响。在这个错误中,Angular告诉你:

你可能用错了方法,你确定你是对的吗?

同一博客还说:

通常,解决方法是使用正确的变更检测钩子来创建一个动态组件


对于我来说,一个简短的指南是在编码时至少考虑以下事情:

(我会尽量在以后补充):

  1. 避免修改父组件的子组件值,
  2. 当你使用@Input@Output指令时,尽量避免触发生命周期变化,除非组件完全初始化。
  3. 避免不必要的this.cdr.detectChanges();调用,它们会触发更多的错误,特别是当你处理大量动态数据时
  4. 当强制使用this.cdr.detectChanges();时,确保使用的变量(@Input, @Output, etc)在正确的检测钩子(OnInit, OnChanges, AfterView, etc)中被填充/初始化
  5. 在可能的情况下,删除而不是隐藏,这与第3点和第4点有关。(与angulardart相同)
  6. 避免在setters中使用@Input注释的任何类型的逻辑,setter在ngAfterViewInit之前执行,所以它很容易触发问题。如果需要的话,最好将逻辑放在ngOnChanges方法中。

如果你想完全理解Angular Life Hook,我建议你阅读这里的官方文档:https://angular.io/guide/lifecycle-hooks

@leocaseiro在github issue上提到过。

我为那些正在寻找简单修复的人找到了3个解决方案。

1)从ngAfterViewInit移动到ngAfterContentInit

2)移动到ngAfterViewChecked结合ChangeDetectorRef作为建议#14748 (comment)

3)继续使用ngOnInit(),但之后调用ChangeDetectorRef.detectChanges()您的更改。

在我们的例子中,我们通过在组件中添加changeDetection并在ngAfterContentChecked中调用detectChanges()来修复,代码如下

@Component({
  selector: 'app-spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpinnerComponent implements OnInit, OnDestroy, AfterContentChecked {
  show = false;
  private subscription: Subscription;
  constructor(private spinnerService: SpinnerService, private changeDedectionRef: ChangeDetectorRef) { }
  ngOnInit() {
    this.subscription = this.spinnerService.spinnerState
      .subscribe((state: SpinnerState) => {
        this.show = state.show;
      });
  }
  ngAfterContentChecked(): void {
      this.changeDedectionRef.detectChanges();
  }
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

我用了很多次的小技巧

Promise.resolve(data).then(() => {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
    this.cdRef.detectChanges();
});

把你的代码从ngAfterViewInit移到ngAfterContentInit

视图在内容之后初始化,因此ngAfterViewInit()ngAfterContentInit()之后被调用

我得到了这个错误,因为我声明了一个变量,后来想
使用ngAfterViewInit

改变它的值
export class SomeComponent {
    header: string;
}

修复从

切换到

ngAfterViewInit() { 
    // change variable value here...
}

ngAfterContentInit() {
    // change variable value here...
}

使用默认的表单值来避免错误

没有使用在ngAfterViewInit()中应用detectChanges()的公认答案(这也解决了我的错误),而是决定为动态所需的表单字段保存默认值,这样当表单稍后更新时,如果用户决定更改表单上的一个选项,则不会改变它的有效性,这会触发新的必需字段(并导致提交按钮被禁用)。

这在我的组件中节省了一小部分代码,并且在我的情况下完全避免了错误。

我认为你能想到的最好和最干净的解决方案是这样的:

@Component( {
  selector: 'app-my-component',
  template: `<p>{{ myData?.anyfield }}</p>`,
  styles: [ '' ]
} )
export class MyComponent implements OnInit {
  private myData;
  constructor( private myService: MyService ) { }
  ngOnInit( ) {
    /* 
      async .. await 
      clears the ExpressionChangedAfterItHasBeenCheckedError exception.
    */
    this.myService.myObservable.subscribe(
      async (data) => { this.myData = await data }
    );
  }
}

用Angular 5.2.9测试

尽管已经有很多答案,而且有一个关于变更检测的非常好的文章的链接,但我还是想在这里给出我的两分意见。我认为检查是有原因的,所以我考虑了我的应用程序的架构,并意识到视图中的变化可以通过使用BehaviourSubject和正确的生命周期钩子来处理。这就是我的解决方案。

  • 我使用了一个第三方组件(fullcalendar),但我也使用了Angular Material,所以尽管我做了一个新的样式插件,但获得外观和感觉有点尴尬,因为自定义日历头是不可能的,除非分叉repo并滚动你自己的。
  • 所以我最终得到了底层JavaScript类,并且需要为组件初始化我自己的日历头。这需要ViewChild在我的父元素被渲染之前被渲染,这不是Angular的工作方式。这就是为什么我将模板所需的值包装在BehaviourSubject<View>(null):

    中。
    calendarView$ = new BehaviorSubject<View>(null);
    

接下来,当我确定视图被检查时,我用@ViewChild:

中的值更新主题。
  ngAfterViewInit(): void {
    // ViewChild is available here, so get the JS API
    this.calendarApi = this.calendar.getApi();
  }
  ngAfterViewChecked(): void {
    // The view has been checked and I know that the View object from
    // fullcalendar is available, so emit it.
    this.calendarView$.next(this.calendarApi.view);
  }

然后,在我的模板中,我只使用async管道。没有黑客攻击和变更检测,没有错误,工作顺利。

如果你需要更多的细节,请不要犹豫,尽管问。

所有以前的解决方案对我都不起作用,这是唯一一个真正起作用的。

错误信息示例:

AppComponent.html:1 ERROR错误:ExpressionChangedAfterItHasBeenCheckedError:表达式已经改变检查完之后。前一个值:'ngIf: true'。当前值:"ngIf:假"。at viewDebugError (core.js:20440)(core.js:20428)…

<

解决方案/strong>在触发问题的组件中导入ChangeDetectorRef, AfterContentChecked然后加上:

 ngAfterContentChecked(): void {
    this.changeDetector.detectChanges();
  }

import { Component, OnInit, ChangeDetectorRef, AfterContentChecked } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { environment } from '@env/environment';
import { LayoutService } from '@app/core/layout/layout.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: []
})
export class AppComponent implements OnInit, AfterContentChecked {
  env = environment;
  title = this.env.appName;
  partials: any = {};
  public constructor(
    private titleService: Title,
    public layout: LayoutService,
    private changeDetector: ChangeDetectorRef,
  ) {}
   ngOnInit() {
    this.titleService.setTitle(this.env.appName);
    
    this.layout.getLayout().subscribe(partials => {
      this.partials = partials;
    });
  }
  ngAfterContentChecked(): void {
    this.changeDetector.detectChanges();
  }
}

不是我的:这是来源https://gist.github.com/vades/a225170304f34f88a7a5d48bf4b1f58c

  • 你可以使用µsetTimeout()在javascript事件循环的下一个宏任务队列中运行属性设置。
  • 然而,在这种情况下使用detectChanges()就足够了。有一堆的原因,当这个错误发生:

本文解释了产生此错误的所有原因以及解决此错误的所有方法:https://blog.simplified.courses/angular-expression-changed-after-it-has-been-checked-error/

最新更新