当组件属性取决于当前日期时间时,如何管理Angular2“检查后表达式已更改”异常


167

我的组件的样式取决于当前日期时间。在我的组件中,我具有以下功能。

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

...但是没有成功。


Answers:


356

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

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

完美的解决方案,谢谢。我注意到它还可以与以下挂钩方法一起使用:ngOnChanges,ngDoCheck,ngAfterContentChecked。那么有没有最好的选择?
AnthonyBrenelière16年

27
这取决于您的用例。如果要在初始化组件时执行某些操作,ngOnInit()通常是第一位。如果代码取决于要渲染的DOM ngAfterViewInit()ngAfterContentInit()下一个选项。ngOnChanges()如果每次更改输入时都应执行代码,则非常合适。ngDoCheck()用于自定义更改检测。其实我不知道ngAfterViewChecked()最好用什么。我认为它在之前或之后被称为ngAfterViewInit()
君特Zöchbauer

2
@KushalJayswal抱歉,根据您的描述无法理解。我建议使用代码来演示您要完成的工作,以创建一个新问题。理想情况下,使用StackBlitz示例。
君特Zöchbauer

4
这也是一个很好的解决方案,如果您的组件状态是基于浏览器的DOM计算的属性,如clientWidth,等
约拿

1
刚刚在ngAfterViewInit上使用,就像一个魅力。
弗朗西斯科·阿莱奥

41

TL; DR

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

尽管这是一种解决方法,但有时很难以更好的方式解决此问题,因此,如果使用此方法,请不要怪自己。没关系。

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


如何避免

通常,此错误通常在您添加某处(甚至在父/子组件中)之后发生ngAfterViewInit。所以第一个问题是要问自己-我可以没有生活ngAfterViewInit吗?也许您将代码移到某个位置(ngAfterViewChecked可能是替代方法)。

示例:[ 链接 ]


同样ngAfterViewInit,影响DOM的异步内容也可能导致这种情况。也可以通过setTimeoutdelay(0)在管道中添加运算符来解决:

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

示例:[ 链接 ]


不错的阅读

关于如何调试它及其发生原因的好文章:链接


2
似乎比所选的解决方案慢
Pete B

6
这不是最好的解决方案,但是该死的总是可行的。选定的答案并不总是有效的(一个人需要非常了解钩子才能使其正常工作)
Freddy Bonda

任何人都知道为什么这样做有效的正确解释是什么?是因为它随后在不同的线程(异步)中运行?
knnhcn

3
@knnhcn javascript中没有其他线程。JS本质上是单线程的。SetTimeout只是告诉引擎在计时器到期后的某个时间执行该功能在这里,计时器为0,这是在现代浏览器为4实际处理,这是足够的时间角度做它的魔力一样的东西:developer.mozilla.org/en-US/docs/Web/API/...
Kilves

26

正如@leocaseiro在github问题上提到的那样。

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

1)从ngAfterViewInit移至ngAfterContentInit

2)按照#14748的建议移至ngAfterViewChecked结合ChangeDetectorRef(评论)

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


1
谁能证明最推荐的解决方案是什么?
Pipo

13

她你去两个解决方案!

1.修改ChangeDetectionStrategy到OnPush

对于此解决方案,您基本上是在告诉angular:

停止检查更改;我只会在我知道有必要的时候做

快速修复:

修改您的组件,以便使用 ChangeDetectionStrategy.OnPush

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

这样,事情似乎不再起作用。这是因为从现在开始,您将必须detectChanges()手动进行Angular调用。

this.cdr.detectChanges();

这是一个帮助我了解ChangeDetectionStrategy权利的链接:https : //alligator.io/angular/change-detection-strategy/

2.了解ExpressionChangedAfterItHasBeenCheckedError

这是tomonari_t 答案的一小部分摘录,内容涉及此错误的原因,我尝试仅包括有助于我理解这一部分的部分。

全文显示了有关此处显示的每个点的真实代码示例。

根本原因是角度生命周期的原因:

每次操作后,Angular都会记住用于执行操作的值。它们存储在组件视图的oldValues属性中。

在完成对所有组件的检查之后,Angular然后开始下一个摘要循环,但是不执行操作,而是将当前值与它在上一个摘要循环中记住的值进行比较。

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

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

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

检查所有子组件

因此,当比较值不同时将引发错误。,博客作者Max Koretskyi说:

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

最后,这是一些通常会导致此错误的现实示例:

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

可以在此处找到每个样本(plunkr),在我的情况下,问题是动态组件实例化。

另外,根据我自己的经验,我强烈建议每个人都避免使用该setTimeout解决方案,在我的情况下,它会导致“几乎”无限循环(我不愿意向您展示如何激发它们的21个调用),

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

您可能以错误的方式执行此操作,确定您是正确的吗?

同一博客还说:

通常,解决方法是使用正确的更改检测挂钩创建动态组件

对我来说,一个简短的指南是在编码时至少考虑以下两个方面(随着时间的推移,我将尝试对其进行补充):

  1. 避免从子组件的父组件中修改父组件的值,而是:从其父组件中修改它们。
  2. 使用@Input@Output指令时,除非组件已完全初始化,否则请尽量避免触发lyfecycle更改。
  3. 避免不必要的调用,this.cdr.detectChanges();因为它们可能引发更多错误,尤其是在处理大量动态数据时
  4. this.cdr.detectChanges();强制使用时,请确保@Input, @Output, etc在右侧检测钩(OnInit, OnChanges, AfterView, etc)处填充/初始化正在使用的变量()。
  5. 如果可能的话,请删除而不是hide,这与第3点和第4点有关。

如果您想全面了解Angular Life Hook,建议您在此处阅读官方文档:


我仍然无法理解为什么改变ChangeDetectionStrategyOnPush固定对我来说。我有一个简单的组件,它有个[disabled]="isLastPage()"。该方法正在读取MatPaginator-ViewChild并返回this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;。分页器无法立即使用,但在使用绑定之后@ViewChild。更改已ChangeDetectionStrategy删除的错误-功能仍然像以前一样存在。不知道我现在有哪些缺点,但是谢谢!
伊戈尔

太好了@Igor!使用的唯一退出OnPush是,您this.cdr.detectChanges()每次要刷新组件时都必须使用。我想您已经在使用它
Luis Limas

9

在我们的案例中,我们通过将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();
  }

}

2

我认为您可以想象的最好,最干净的解决方案是:

@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测试


3
这是骇客的..所以毫不客气。
JoeriShoeby '18年

1
@JoeriShoeby上述所有其他解决方案都基于一个定时器或高级角事件的解决方法......这个解决方案是所有专业支持纯ES2017浏览器 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... 访问caniuse.com即可/#search = await
哈维尔·富恩特斯(JavierFuentes)'18年

1
例如,如果您要使用无法使用ES2017功能的Angular + Cordova构建移动应用程序(针对Android API 17)怎么办?注意:接受的答案是一个解决方案,而不是一个解决..
JoeriShoeby

@JoeriShoeby使用打字稿,它被编译为JS,因此没有功能支持问题。
biggbest

@biggbest这是什么意思?我知道Typescript将被编译为JS,但这不能使您能够假定所有JS都可以工作。请注意,Javascript是在网络浏览器中运行的,每个浏览器的行为都不同。
JoeriShoeby

2

我经常使用的小工具

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

我主要用控制器中使用的一些变量替换“ null”。


1

尽管已经有很多答案,并且可以找到有关更改检测的非常好的文章的链接,但是我想在这里给我两分钱。我认为检查是有原因的,所以我考虑了我的应用程序的体系结构,并意识到可以使用来处理视图中的更改BehaviourSubject和正确的生命周期挂钩。这就是我为解决方案所做的事情。

  • 我使用第三方组件(fullcalendar),但我也使用 Angular Material,因此尽管我制作了新的样式插件,但外观和感觉还是有些尴尬,因为如果不分叉仓库就无法自定义日历标题然后自己动手
  • 因此,我最终获得了底层的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管道。不会因更改检测而被黑客入侵,不会出现任何错误,并且运行平稳。

请随时询问是否需要更多详细信息。


1

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

我决定不使用在ngAfterViewInit()中应用detectChanges()的公认答案(这也解决了我的问题),而是决定为动态需要的表单字段保存默认值,以便以后更新表单时,如果用户决定更改表单上的一个选项,该选项将触发新的必填字段(并导致“提交”按钮被禁用),则其有效性不会更改。

这在我的组件中节省了一些代码,在我的情况下,完全避免了该错误。


这是一个非常好的做法,您可以避免不必要的电话this.cdr.detectChanges()
Luis Limas

1

我收到该错误的原因是我声明了一个变量,后来想使用来
更改它的值ngAfterViewInit

export class SomeComponent {

    header: string;

}

修复我从

ngAfterViewInit() { 

    // change variable value here...
}

ngAfterContentInit() {

    // change variable value here...
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.