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

Posted

技术标签:

【中文标题】当组件属性取决于当前日期时间时,如何管理Angular2“检查后表达式已更改”异常【英文标题】:How to manage Angular2 "expression has changed after it was checked" exception when a component property depends on current datetime 【发布时间】:2021-11-30 00:06:09 【问题描述】:

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

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

...但没有成功。

【问题讨论】:

这可能有助于理解问题:Angular Debugging "Expression has changed after it was checked": Simple Explanation (and Fix) 【参考方案1】:

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

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。那么有没有最好的选择呢? 这取决于您的用例。如果你想在组件初始化的时候做点什么,ngOnInit() 通常是第一位的。如果代码依赖于渲染的 DOM,ngAfterViewInit()ngAfterContentInit() 是下一个选项。如果每次更改输入时都应执行代码,ngOnChanges() 非常适合。 ngDoCheck() 用于自定义更改检测。其实我不知道ngAfterViewChecked() 最适合做什么。我认为它是在 ngAfterViewInit() 之前或之后调用的。 我正在尝试更改 this.value 以防止设置无效值,但是此处描述的技术对我不起作用。还有其他方法我应该这样做吗? @KushalJayswal 抱歉,您的描述无法理解。我建议使用演示您尝试完成的代码的代码创建一个新问题。理想情况下使用 StackBlitz 示例。 如果您的组件状态是基于浏览器计算的 DOM 属性,例如 clientWidth 等,这也是一个很好的解决方案。【参考方案2】:

TL;DR

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

虽然这是一种解决方法,但有时很难以任何更好的方式解决此问题,所以如果您正在使用这种方法,请不要责怪自己。没关系。

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


如何避免

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

示例:[link]


还有

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

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

示例:[link]


阅读愉快

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

【讨论】:

似乎比选择的解决方案要慢 这不是最好的解决方案,但该死的,这总是有效的。选择的答案并不总是有效(需要很好地理解钩子才能使其工作) 这行得通的正确解释是什么,有人知道吗?是因为它然后在不同的线程(异步)中运行吗? @knnhcn javascript 中没有不同的线程。 JS 本质上是单线程的。 SetTimeout 只是告诉引擎在 定时器到期后的某个时间执行函数。 这里的定时器为 0,在现代浏览器中实际上被视为 4,这有足够的时间让 angular 发挥它的魔力东西:developer.mozilla.org/en-US/docs/Web/API/… @FreddyBonda 我得到了同样的结果。我通常在“ngAfterViewInit”中订阅需要与 DOM 交互的 Observable,因为其他钩子在某些情况下不起作用。每次我这样做时,我通常都会收到报告的错误,摆脱这种情况的唯一方法是向可观察管道添加延迟(0)。不知道设计好不好,只要能用就行....【参考方案3】:

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

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

1) 从ngAfterViewInit 移动到ngAfterContentInit

2) 移动到 ngAfterViewCheckedChangeDetectorRef 结合为 建议在 #14748(评论)

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

【讨论】:

谁能记录下最推荐的解决方案是什么?【参考方案4】:

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

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

【讨论】:

这太老套了 .. 太没必要了。 @JoeriShoeby 上述所有其他解决方案都是基于 setTimeouts 或高级 Angular 事件的解决方法...此解决方案是纯 ES2017,所有主要浏览器都支持 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…caniuse.com/#search=await 如果您正在使用 Angular + Cordova 构建移动应用程序(以 android API 17 为目标),而您不能依赖 ES2017 功能,该怎么办?请注意,接受的答案是解决方案,而不是解决方法.. @JoeriShoeby 使用打字稿,编译成 JS,所以没有功能支持问题。 @biggbest 你是什么意思?我知道 Typescript 会被编译成 JS,但这并不能让你假设所有的 JS 都可以工作。请注意,Javascript 在网络浏览器中运行,每个浏览器在其上的行为都不同。【参考方案5】:

一个我用过很多次的小作品

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

【讨论】:

【参考方案6】:

这里有两个解决方案!


1。将 ChangeDetectionStrategy 修改为 OnPush

对于这个解决方案,你基本上是在告诉 Angular:

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

修改您的组件,使其使用ChangeDetectionStrategy.OnPush,如下所示:

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

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

this.cdr.detectChanges();

如果您有兴趣,请查看this article。它帮助我了解了ChangeDetectionStrategy 的工作原理。


2。了解 ExpressionChangedAfterItHasBeenCheckedError

这是来自this article 的关于此错误原因的一小段摘录,我已尝试仅包含有助于我理解这一点的部分。

完整的文章展示了关于此处显示的每一点的真实代码示例。

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

在每次操作之后,Angular 都会记住它曾经执行过的值 一个手术。它们存储在 组件视图。

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

在摘要周期检查以下操作:

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

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

检查所有子组件

因此,当比较的值不同时会引发错误。,博主Max Koretskyi 表示:

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

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

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

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

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

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

你可能做错了,你确定你是对的吗?

同样的博客还说:

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


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

我会随着时间的推移尝试补充它):

    避免从其子组件修改父组件值, 而是:从它们的父级修改它们。 当您使用 @Input@Output 指令时,请尽量避免触发生命周期更改,除非组件已完全初始化。 避免不必要的this.cdr.detectChanges(); 调用,它们会触发更多错误,尤其是在处理大量动态数据时 当必须使用this.cdr.detectChanges(); 时,请确保正在使用的变量 (@Input, @Output, etc) 在正确的检测挂钩 (OnInit, OnChanges, AfterView, etc) 处填充/初始化 如果可能,remove rather than hide,这与第 3 点和第 4 点有关。(更新:我今天注意到 Angular 建议删除而不是隐藏已从他们的网页中删除,here's the same quote but for angulardart) 避免在setters 中使用@Input 注释的任何类型的逻辑,setter 在ngAfterViewInit 之前执行,因此很容易触发问题。

还有

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

【讨论】:

我仍然无法理解为什么将 ChangeDetectionStrategy 更改为 OnPush 会为我修复它。我有一个简单的组件,它有[disabled]="isLastPage()"。该方法是读取MatPaginator-ViewChild 并返回this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;。分页器无法立即使用,但在使用@ViewChild 绑定后。更改 ChangeDetectionStrategy 消除了错误 - 功能仍然像以前一样存在。不知道我现在有哪些缺点,但谢谢! 太棒了@Igor!使用OnPush 的唯一好处是每次您希望组件刷新时都必须使用this.cdr.detectChanges()。我猜你已经在使用它了【参考方案7】:

在我们的例子中,我们通过在组件中添加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();
  


【讨论】:

这就是答案,这有助于&lt;button [disabled]="functionCall()"&gt;【参考方案8】:

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

我使用第三方组件(fullcalendar),但我也使用Angular Material,所以虽然我制作了一个新的样式插件,但获得外观和感觉有点尴尬,因为日历标题的自定义不是无需分叉回购并自行滚动即可。

所以我最终得到了底层的 JavaScript 类,并且需要为组件初始化我自己的日历标题。这需要在渲染我的父级之前渲染 ViewChild,这不是 Angular 的工作方式。这就是我将模板所需的值包装在 BehaviourSubject&lt;View&gt;(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 管道。没有更改检测的黑客攻击,没有错误,工作顺利。

如果您需要更多详细信息,请随时询问。

【讨论】:

【参考方案9】:

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

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

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

【讨论】:

这是一个非常好的做法,这样可以避免不必要地调用this.cdr.detectChanges()【参考方案10】:

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

export class SomeComponent 

    header: string;


解决我切换的问题

ngAfterViewInit()  

    // change variable value here...

ngAfterContentInit() 

    // change variable value here...

【讨论】:

【参考方案11】:

将您的代码从 ngAfterViewInit 移动到 ngAfterContentInit。

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

【讨论】:

以上是关于当组件属性取决于当前日期时间时,如何管理Angular2“检查后表达式已更改”异常的主要内容,如果未能解决你的问题,请参考以下文章

在当前日期之前/之后列出具有日期属性的文章

从firestore查询数据时,如何将保存的字符串格式的日期与当前日期进行比较?

使用 Jest 测试 Vue3 组件时如何模拟计算属性

如何获取当前日期(没有小时和分钟)?

声明状态属性的组件是未定义的,即使它是,Auth

在我当前订阅结束之前升级到更高的订阅时,订阅如何工作?