请向我解释为什么我一直得到这个错误:ExpressionChangedAfterItHasBeenCheckedError:表达式已经改变后,它被检查。

显然,我只有在开发模式下才会遇到这种情况,在我的产品构建中不会出现这种情况,但这非常烦人,而且我根本不明白在我的开发环境中出现错误而不会在prod上显示的好处——可能是因为我缺乏理解。

通常,修复很简单,我只是把导致错误的代码包装在setTimeout中,就像这样:

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

或者使用如下构造函数强制检测更改:

this.isLoading = true;
this.cd.detectChanges();

但是为什么我总是遇到这个错误呢?我想要了解它,这样我就可以在将来避免这些俗套的修复。


当前回答

以下是我对正在发生的事情的看法。我还没有阅读文档,但我确信这是错误显示的部分原因。

*ngIf="isProcessing()" 

当使用*ngIf时,它会在每次条件改变时通过添加或删除元素来物理地改变DOM。因此,如果条件在呈现给视图之前发生了变化(这在Angular的世界中是极有可能的),就会抛出错误。请参阅开发和生产模式之间的解释。

[hidden]="isProcessing()"

当使用[hidden]时,它不会在物理上改变DOM,而只是从视图中隐藏元素,很可能在后面使用CSS。元素仍然在DOM中,但不可见,这取决于条件的值。这就是为什么使用[hidden]时不会出现错误的原因。

其他回答

Angular会运行更改检测,当它发现传递给子组件的某些值被更改时,Angular会抛出以下错误:

点击查看更多信息

为了纠正这个问题,我们可以使用AfterContentChecked生命周期钩子和

import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }

如果你在ngAfterViewInit()中触发EventEmitter时看到ExpressionChangedAfterItHasBeenCheckedError,那么你可以在创建EventEmitter时传递true,使其在当前更改检测周期后异步发射。

@Component({
  ...
})
export class MyComponent implements AfterViewInit {
  // Emit asynchronously to avoid ExpressionChangedAfterItHasBeenCheckedError
  @Output() valueChange = new EventEmitter<number>(true);

  ...

  ngAfterViewInit(): void {
    ...
    this.valueChange.emit(newValue);
    ...
  }

}

@HostBinding可能是这个错误的一个令人困惑的来源。

例如,假设您在组件中有以下主机绑定

// image-carousel.component.ts
@HostBinding('style.background') 
style_groupBG: string;

为了简单起见,我们假设这个属性是通过以下输入属性更新的:

@Input('carouselConfig')
public set carouselConfig(carouselConfig: string) 
{
    this.style_groupBG = carouselConfig.bgColor;   
}

在父组件中,你通过编程方式在ngAfterViewInit中设置它

@ViewChild(ImageCarousel) carousel: ImageCarousel;

ngAfterViewInit()
{
    this.carousel.carouselConfig = { bgColor: 'red' };
}

事情是这样的:

Your parent component is created The ImageCarousel component is created, and assigned to carousel (via ViewChild) We can't access carousel until ngAfterViewInit() (it will be null) We assign the configuration, which sets style_groupBG = 'red' This in turn sets background: red on the host ImageCarousel component This component is 'owned' by your parent component, so when it checks for changes it finds a change on carousel.style.background and isn't clever enough to know that this isn't a problem so it throws the exception.

一种解决方案是在ImageCarousel内部引入另一个包装器div,并在其上设置背景颜色,但这样就无法获得使用HostBinding的一些好处(例如允许父类控制对象的完整边界)。

在父组件中,更好的解决方案是在设置配置之后添加detectChanges()。

ngAfterViewInit()
{
    this.carousel.carouselConfig = { ... };
    this.cdr.detectChanges();
}

这看起来很明显,和其他答案很相似,但有细微的区别。

考虑这样一种情况,您直到开发的后期才添加@HostBinding。突然你得到这个错误,它似乎没有任何意义。

在@ViewChild装饰器中添加static: true

@ViewChild(UploadComponent, { static: true }) uploadViewChild: UploadComponent;

更新

我强烈建议首先从OP的自我响应开始:适当地考虑在构造函数中可以做什么,而在ngOnChanges()中应该做什么。

原始

这与其说是一个答案,不如说是一个题外话,但它可能会帮助到一些人。当我试图让按钮的存在取决于表单的状态时,我偶然发现了这个问题:

<button *ngIf="form.pristine">Yo</button>

据我所知,这种语法会导致根据条件从DOM中添加和删除按钮。这又会导致ExpressionChangedAfterItHasBeenCheckedError。

在我的案例中,解决方案(尽管我没有声称掌握了差异的全部含义)是使用display: none来代替:

<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>