ngIf - 表达式在被检查后发生了变化

87

我有一个简单的场景,但是就是无法让它工作!

我在视图中显示一些文本,内容被限制在一个固定高度的框中。

这些文本是从服务器获取的,所以当文本到来时,视图会更新。

现在我有一个“展开”按钮,它有一个ngIf指令,如果框中的文本溢出,则ngIf指令应该显示按钮。

问题在于,由于文本在获取后会发生变化,所以当Angular的变更检测完成后,“展开”按钮的条件会变为true...

所以我会得到这个错误:表达式已经变更,检查后仍然改变了。之前值:'false'。当前值:'true'。

显然,按钮不会显示...

请查看此Plunker(检查控制台以查看错误...)

有什么办法可以让它正常工作吗?


2
请参考以下链接:https://github.com/angular/angular/issues/6005#issuecomment-165905348 - Michael Doye
11个回答

117

这个错误是因为您处于dev mode

dev mode下,变更检测会在每次正常的变更检测运行后再添加一个额外的回合来检查模型是否已更改。

因此,要强制进行变更检测并在下一个时钟周期中运行,我们可以像这样操作:

export class App implements AfterViewChecked {

  show = false; // add one more property
  
  constructor(private cdRef : ChangeDetectorRef) { // add ChangeDetectorRef
    //...
  }
  //...
  ngAfterViewChecked() {
    let show = this.isShowExpand();
    if (show != this.show) { // check if it change, tell CD update view
      this.show = show;
      this.cdRef.detectChanges();
    }
  }
  
  isShowExpand()
  {
    //...
  }
}

演示链接:https://plnkr.co/edit/UDMNhnGt3Slg8g5yeSNO?p=preview


8
这对我很有效。在我的ngAfterViewChecked方法中,我只有这个.cdRef.detectChanges();。 - Ziggler
5
与@Ziggler相同,只需在ngAfterViewChecked中加入this.cdRef.detectChanges();代码即可使它对我也有效。 - Lautre
我还是不明白这个问题....如果Angular发现模型已经改变,为什么它不直接更新视图,而是抛出一个错误呢? - undefined

28

在 ngAfterContentChecked 后使变更检测器运行解决了我的问题

示例如下:

import { ChangeDetectorRef,AfterContentChecked} from '@angular/core'
export class example implements OnInit, AfterContentChecked {
    ngAfterContentChecked() : void {
        this.changeDetector.detectChanges();
    }
}

尽管我阅读了一些文章,这个问题在生产模式下得到了解决,而不需要任何必要的修复。

以下是这种问题可能出现的原因:

它强制实施单向数据流:当我们的控制器类上的数据得到更新时,变更检测会运行并更新视图。

但是,视图的更新本身不会触发进一步的更改,这些更改反过来会触发对视图的进一步更新。

https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/


唯一的问题是ngAfterContentChecked在每次ngDoCheck之后运行,这意味着它会在几秒钟内运行数百次,并可能导致页面渲染变慢。请尝试通过在ngAfterViewInit中调用"this.changeDetector.detectChanges()"来解决错误。 - Dinesh Khetarpal

22

实现AfterContentChecked方法。

constructor(
    private cdr: ChangeDetectorRef,
) {}

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

4
这个对我来说很有感触 <3 - WtFudgE

19

由于某些原因,@Tiep Phan的答案对我来说无法强制进行变更检测,但是使用setTimeout(也可以强制进行变更检测)确实可行。

我只需要将其添加到有问题的那一行即可,在ngOnInit中已经有了现有代码,不必添加ngAfterViewInit。

示例:

ngOnInit() {
    setTimeout(() => this.loadingService.loading = true);
    asyncFunctionCall().then(res => {
        this.loadingService.loading = false;
    })
}

更多细节请参见:https://github.com/angular/angular/issues/6005


6
我们可以通过将更改检测策略更改为OnPush来避免抛出ExpressionChangedAfterItHasBeenCheckedError错误。因此,不会执行额外的更改检测,因此也不会抛出错误。
要实现此操作,您需要在以下.ts文件中作为@Component装饰器的一部分添加ChangeDetectionStrategy.OnPush:
@Component({
  selector: 'your-component',
  templateUrl: 'your-component.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})

1
老兄,你救了我的一天!! - Cabuxa.Mapache
@Cabuxa.Mapache 很高兴保存。 - Steffi Keran Rani J

6
为了解决这个问题,你可以将更改 *ngIf 状态的变量从 ngAfterViewInit 中移动到 ngOnInitconstructor。因为在调用 AfterViewInit 方法时不允许更改 html 的状态。
正如 @tiep-phan 所说,另一种方法是在构造函数中传递 ChangeDetectorRef,并在 AfterViewInit 方法中更改 *ngIf 的状态后调用 this.chRef.detectChanges()。

提醒自己:我在ngoninit中调用了一个异步函数来获取数据,但没有等待该函数。 - Ranga

2

这是针对 Angular 9+ 版本的:

当使用 @ViewChild('templateRefVar') xyz!: TemplateRef 时,也会出现此错误。原因是,显然引用在视图准备好之前是 undefind,导致当值实际上更改为模板引用时出现此错误。

如果您知道您的模板引用存在,因为您有例如:xyz.html:

...
<ng-template #templateRefVar>
  ...
</ng-template>
...

您可以像这样将其声明为static

@ViewChild('templateRefVar', {static: true}) xyz!: TemplateRef

"引入此选项是为了支持动态创建嵌入式视图。" - angular.io guide

有关如何进行进一步阅读,请参见以下内容:


我无法感谢你们的足够!!!我遇到了@ViewChild()的问题,尝试了ngAfterContentChecked()的hack方法,但并不满意,它只会减慢应用程序的速度。 您的解决方案是解决错误的正确方式。再次感谢。 - Jaime Vasquez

1

对于任何遇到类似问题的人,基本上你正在尝试在不应该的地方更新dom,在这个链接https://blog.angular-university.io/angular-debugging/中,你可以找到有关如何调试、查找生成问题的确切代码片段以及一些解决方法的详细信息。


0

Angular的变更检测过程是同步的,为了避免这个错误,我们可以将其改为异步过程,并通过执行类似以下代码的方式使其在变更检测之外运行。 我们可以将此片段放置在onInit或Constructor方法中。

this.ngZone.runOutsideAngular(() => {    
     this.interval = window.setInterval(() => {        

         //logic which we want to handle

     }, 1)});

0
我收到了这个错误,因为我在加载时打开了一个包含被检查后被更改的对象的Ngbmodal弹出窗口。我通过在setTimeout内调用模态打开函数来解决了这个问题。
setTimeout(() => {
  this.modalReference = this.modalService.open(this.modal, { size: "lg" });
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接