检查表达式___后已更改


286

为什么在这个简单的组件普拉克

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message:string = 'loading :(';

  ngAfterViewInit() {
    this.updateMessage();
  }

  updateMessage(){
    this.message = 'all done loading :)'
  }
}

抛出:

例外:表达式'我在App @ 0:5中是{{message}}”已被检查。先前的值:[我在App @ 0:5的{{message}}}中的“我正在加载:('。当前值:'我已经完成加载:)”

当我正在做的所有事情都是在启动视图时更新一个简单的绑定吗?



ChangeDetectionStrategy使用detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas

试想一下,有一个输入控件,并且您正在使用一种方法向其中填充数据,并且使用相同的方法为其分配了一些值。编译器肯定会对new / previous值感到困惑。因此,绑定和填充应该以不同的方法进行。
MAC

Answers:


292

如drewmoore所述,这种情况下的正确解决方案是手动触发当前组件的更改检测。这是使用对象的detectChanges()方法ChangeDetectorRef(从导入angular2/core)或其markForCheck()方法来完成的,该方法也使所有父组件更新。相关示例

import { Component, ChangeDetectorRef, AfterViewInit } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App implements AfterViewInit {
  message: string = 'loading :(';

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.message = 'all done loading :)'
    this.cdr.detectChanges();
  }

}

这里同时Plunkers展示ngOnInitsetTimeout的,并且enableProdMode以防万一方法。


7
就我而言,我正在打开一个模态。打开模态后,它显示消息“表达式___在检查后已更改”,因此我的解决方案添加了this.cdr.detectChanges();。打开我的模态后。谢谢!
豪尔赫·卡萨列戈

您对cdr属性的声明在哪里?我希望cdr : anymessage声明下看到类似这样的行。只是担心我错过了什么?
CodeCabbie'3

1
@CodeCabbie它在构造函数参数中。
slasky

1
此分辨率解决了我的问题!非常感谢!非常清晰和简单的方法。
lwozniak

3
这对我有所帮助-进行了一些修改。我必须设置通过ngFor循环生成的li元素的样式。我需要在单击“排序”时根据innerText更改列表项中跨度的颜色,这会更新一个用作排序管道参数的布尔值(排序后的结果是数据的副本,因此样式无法获取仅使用ngStyle更新)。-我没有使用'AfterViewInit',而是使用了'AfterViewChecked'-我还确保导入实现 AfterViewChecked。 注意:将管道设置为“ pure:false”不起作用,我不得不添加此额外的步骤(:
Chloe Corrigan

170

首先,请注意,只有在以开发人员模式运行应用程序时才会引发此异常(默认情况是beta-0起是这种情况):如果enableProdMode()在引导应用程序时调用,则不会引发该异常(请参阅更新的代码)。

其次,不要这样做,因为有充分的理由抛出此异常:简而言之,在开发人员模式下,每轮变更检测之后都会立即进行第二轮,以验证自第一轮结束以来没有任何绑定发生更改,因为这将表明更改是由更改检测本身引起的。

在你的pl缩中 {{message}}通过对的调用来更改调用setMessage()发生在ngAfterViewInit钩子中,该钩子是初始更改检测回合的一部分。但这本身并不成问题-问题在于setMessage()更改绑定但不会触发新一轮的更改检测,这意味着只有在将来其他地方触发一轮更改检测之前,才会检测到此更改。

外卖: 更改绑定的任何内容都需要在更改触发一轮更改检测

更新以响应所有请求,以提供有关如何执行此操作的示例:@Tycho 的解决方案有效,答案 @MarkRajcok中指出的三种方法也是如此。但坦率地说,他们都对我感到丑陋和不对劲,就像我们习惯于在ng1中依靠的那种骇客一样。

可以肯定的是,偶尔会有情况下,其中这些黑客是适当的,但如果你在任何使用它们多了一个偶然的基础上,这是一个迹象,表明你战斗的框架,而不是完全拥抱它的被动性。

恕我直言,一种更惯用的“ Angular2方式”可以解决以下问题:(plunk

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message | async}} </div>`
})
export class App {
  message:Subject<string> = new BehaviorSubject('loading :(');

  ngAfterViewInit() {
    this.message.next('all done loading :)')
  }
}

13
为什么setMessage()不触发新一轮的更改检测?我以为Angular 2在您更改UI中某些东西的值时会自动触发更改检测。
丹尼·利宾

4
@drewmoore“更改绑定的任何内容都需要在更改时触发一轮更改检测”。怎么样?这是一个好习惯吗?难道不是所有事情都可以一次完成吗?
Daniel Birowsky Popeski 2015年

2
@Tycho,的确如此。自从我发表评论以来,我已经回答了另一个问题,我在其中描述了运行变更检测3种方法,其中包括detectChanges()
Mark Rajcok '16

4
只是要注意,在当前问题正文中,被调用的方法名为updateMessage,而不是setMessage
superjos 16/08/22

3
@Daynil,我有相同的感觉,直到我阅读了在以下问题下发表评论的博客:blog.angularindepth.com/… 它解释了为什么需要手动进行此操作。在这种情况下,角度变化检测具有生命周期。如果在这些生命周期之间更改了某个值,则需要运行强制更改检测(或settimeout-在下一个事件循环中执行,再次触发更改检测)。
Mahesh

50

ngAfterViewChecked() 为我工作:

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

constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
   //your code to update the model
   this.cdr.detectChanges();
}

正如提请注意的那样,角度的变化检测周期为两个阶段,必须在修改儿童视图后检测变化,我觉得最好的方法是使用角度自身提供的生命周期挂钩,并手动请求角度检测更改并将其绑定。我个人认为这似乎是一个适当的答案。
Joey587 '19

1
这项工作对我来说,是为了将层次结构动态组件一起加载到内部。
Mehdi Daustany

47

我通过从角铁芯添加ChangeDetectionStrategy来解决此问题。

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})

这对我有用。我不知道这和使用ChangeDetectorRef有什么区别
Ari

1
嗯...然后,变更检测器的模式将最初设置为CheckOnce文档
Alex Klaus

是的,错误/警告已消失。但是它比以前花费了很多时间,例如5-7秒的差异,这是巨大的。
卡皮尔·拉格万希

1
@KapilRaghuwanshi this.cdr.detectChanges();在尝试加载任何内容后运行。因为可能是因为变更检测未触发
Harshal Carpenter

36

ngOnInit您只是因为更改了成员变量而不能使用message吗?

如果要访问对子组件的引用@ViewChild(ChildComponent),则确实需要使用ngAfterViewInit

肮脏的解决方法是updateMessage()在下一个事件循环中使用setTimeout 调用。

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}

4
将代码更改为ngOnInit方法对我有用。
法利恩

29

为此,我已经尝试了以上答案,但许多版本在Angular的最新版本(6或更高版本)中均不起作用

我正在使用Material控件,该控件在完成第一次绑定后需要进行更改。

    export class AbcClass implements OnInit, AfterContentChecked{
        constructor(private ref: ChangeDetectorRef) {}
        ngOnInit(){
            // your tasks
        }
        ngAfterContentChecked() {
            this.ref.detectChanges();
        }
    }

因此,请添加我的答案,这有助于解决某些特定问题。


这实际上适合我的情况,但是您有一个错字,在实现AfterContentChecked之后,您应该调用ngAfterContentChecked,而不是ngAfterViewInit。
Tomas Lukac

我目前正在使用8.2.0版:)
Tomas Lukac,

1
@TomášLukáč感谢您的答复,我已经更新了答案(y)
MarmiK,

1
如果以上方法均无效,我建议您使用此答案。
Amir Choubani

每次重新计算或重新检查DOM时都会调用afterContentChecked。从Angular版本9中获得
启发

24

这篇文章您需要了解的有关ExpressionChangedAfterItHasBeenCheckedError错误的一切都非常详细地解释了该行为。

设置的问题是,ngAfterViewInit生命周期挂钩在更改检测处理的DOM更新之后执行。并且您正在有效地更改此挂钩中模板中使用的属性,这意味着需要重新渲染DOM:

  ngAfterViewInit() {
    this.message = 'all done loading :)'; // needs to be rendered the DOM
  }

这将需要另一个变更检测周期,而Angular设计仅运行一个摘要周期。

基本上,有两种解决方法:

  • 异步更新属性或者使用setTimeoutPromise.then或在模板异步观察的引用

  • 在DOM更新之前执行钩子中的属性更新-ngOnInit,ngDoCheck,ngAfterContentInit,ngAfterContentChecked。


阅读您的文章:blog.angularindepth.com/…,将很快阅读另一个blog.angularindepth.com/…。仍然不知道该问题的解决方案。你能告诉我如果我添加ngDoCheck或ngAfterContentChecked生命周期挂钩并将其添加到this.cdr.markForCheck();会发生什么情况?(用于ChangeDetectorRef的CDR)。这是在生命周期挂钩并随后完成检查之后检查更改的正确方法。
Harsimer

9

您只需要在正确的生命周期挂钩中更新消息即可,在这种情况下,它是ngAfterContentChecked而不是ngAfterViewInit,因为在ngAfterViewInit中,变量消息的检查已经开始但尚未结束。

请参阅:https : //angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#afterview

所以代码将是:

import { Component } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message: string = 'loading :(';

  ngAfterContentChecked() {
     this.message = 'all done loading :)'
  }      
}

请参阅Plunker上的工作演示


我正在使用由Observable填充@ViewChildren()的基于长度的计数器的组合。这是唯一对我有用的解决方案!
msanford

2
结合ngAfterContentCheckedChangeDetectorRef以上从为我工作。在ngAfterContentChecked所谓的-this.cdr.detectChanges();
注:Kunal与Dethe

6

之所以会出现此错误,是因为初始化后会立即更新现有值。因此,如果您在DOM中呈现现有值后更新新值,则它将正常工作。就像本文中提到的Angular Debugging“检查表达式后更改了它”

例如你可以使用

ngOnInit() {
    setTimeout(() => {
      //code for your new value.
    });

}

要么

ngAfterViewInit() {
  this.paginator.page
      .pipe(
          startWith(null),
          delay(0),
          tap(() => this.dataSource.loadLessons(...))
      ).subscribe();
}

如您所见,我在setTimeout方法中没有提到时间。由于它是浏览器提供的API,而不是JavaScript API,因此它将在浏览器堆栈中单独运行,并等待直到调用堆栈项完成。

菲利普·罗伯茨(Philip Roberts)在Youtube视频之一中解释了浏览器API调用概念的方式(事件循环是什么?)。


4

您还可以在ngOnInt()-Method中将对updateMessage()的调用放入,至少对我有用

ngOnInit() {
    this.updateMessage();
}

在RC1中,这不会触发异常


3

您也可以使用rxjs Observable.timer函数创建计时器,然后更新订阅中的消息:                    

Observable.timer(1).subscribe(()=> this.updateMessage());

2

引发错误是因为调用ngAfterViewInit()时,您的代码已更新。意味着当ngAfterViewInit发生时,您的初始值已更改。如果在ngAfterContentInit()中调用它,则不会引发错误。

ngAfterContentInit() {
    this.updateMessage();
}

0

由于我没有足够的声誉,所以我无法评论@Biranchi的帖子,但这为我解决了这个问题。

要注意的一件事!如果添加changeDetection:该组件上的ChangeDetectionStrategy.OnPush不起作用,并且其子组件(哑组件)也尝试将其添加到父组件。

这修复了该错误,但我想知道它的副作用是什么。


0

使用数据表时出现类似错误。当您在另一个* ngFor数据表中使用* ngFor时,会发生此错误,因为它拦截了角度更改周期。因此,而不是在数据表中使用数据表,而要使用一个常规表或将mf.data替换为数组名称。这很好。


0

我认为最简单的解决方案如下:

  1. 进行一种将值分配给某个变量的实现,即通过函数或设置器。
  2. (static working: boolean)在存在该函数的类中创建一个类变量,每次调用该函数时,只要使它随便即可即可。在该函数中,如果working的值为true,则无需执行任何操作即可立即返回。否则,执行所需的任务。确保在任务完成后将该变量更改为false,即在代码行的末尾或在完成分配值后在subscription方法内!
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.