在检查后,表达式 ___ 发生了变化。

444

为什么在这个简单的plunk中的组件会出现这种情况


@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}}]

当我只是在视图初始化时更新一个简单绑定时发生了什么?


10
这篇文章 关于 ExpressionChangedAfterItHasBeenCheckedError 错误,你需要了解的一切 详细地解释了其行为。 - Max Koretskyi
考虑在使用detectChanges()时修改您的ChangeDetectionStrategy。https://dev59.com/JlkS5IYBdhLWcg3we22Z#54954374 - luiscla27
1
仅考虑一个输入控件并在一个方法中填充数据的情况,同时在同一方法中为其分配某个值。编译器肯定会对新/旧值感到困惑。因此,绑定和填充应该在不同的方法中进行。 - Ram
正如@Jo-VdB所问,难道你不能使用ngOnInit()来更新绑定吗? - oomer
23个回答

436

正如drewmoore所说,这种情况下的正确解决方案是手动触发当前组件的变更检测。这可以使用ChangeDetectorRef对象(从angular2/core导入)的detectChanges()方法或其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();
  }

}

如果需要的话,这里也有一些 Plunker 展示 ngOnInit, setTimeout, 和 enableProdMode 的方法。


12
在我的情况下,我正在打开一个模态框。在打开模态框后,它显示了消息“检查后表达式 ____ 已更改”,所以我的解决方案是在打开模态框之后添加 this.cdr.detectChanges(); 。谢谢! - Jorge Casariego
1
@CodeCabbie 它在构造函数参数中。 - slasky
3
这个决议解决了我的问题!非常感谢!表述清晰简单易懂。 - wozniaklukasz
8
这对我有所帮助 - 经过一些修改。我必须为通过ngFor循环生成的li元素进行样式设置。我需要根据innerText更改列表项内部span的颜色,这取决于点击“排序”,该操作更新了一个用作排序管道参数的布尔值(排序结果是数据的副本,因此仅使用ngStyle无法更新样式)。
  • 我使用了**'AfterViewChecked'**而不是使用'AfterViewInit'。
  • 我还确保导入实现了AfterViewChecked。注意:将管道设置为'pure: false'没有起作用,我还必须添加这个额外的步骤(:
- Chloe Corrigan
1
该框架本身应该知道更改发生在生命周期方法(ngAfterViewInit)中,并且可以自行调用额外的变更检测。留给框架用户去绑定松散的末尾是很差的设计。摸头皱眉表情。 - rapt
显示剩余5条评论

221

首先,请注意,只有在以开发模式运行应用程序时(从beta-0开始默认情况下),才会抛出此异常:如果您在引导应用程序时调用 enableProdMode(),则不会抛出异常(请参见更新的Plunk)。

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

在您的Plunk中,绑定{{message}}通过调用setMessage()而发生变化,该函数在ngAfterViewInit钩子中触发,作为初始变更检测的一部分。虽然这本身并不会造成问题,但问题在于setMessage()改变了绑定,但没有触发新的变更检测轮次,这意味着直到将来触发某个其他的变更检测轮次时才会检测到这种变化。

要点是:任何改变绑定的事情都需要在其发生时触发一轮变更检测

更新以回应所有请求一个如何做到这一点的示例:@Tycho的解决方案有效,答案中@MarkRajcok指出的三种方法也是有效的。但坦率地说,它们都感觉很丑陋、不正确,就像我们习惯于在ng1中依赖的那种hack。

当然,有一些偶尔的情况下这些hack是适用的,但如果您经常使用它们,这表明您正在与框架作斗争,而不是完全拥抱其反应性质。

在我看来,更符合惯例的“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 :)')
  }
}

20
为什么调用setMessage()不会触发新的变更检测?我以为在Angular 2中,当你改变UI元素的值时,自动触发变更检测。 - Daynil
5
“任何更改绑定的操作都需要在其发生时触发一轮变更检测。” 如何触发?这是一种好做法吗?难道不应该在单次运行中完成所有操作吗? - Daniel Birowsky Popeski
2
@Tycho,确实。自从我写下那个评论以来,我已经回答了另一个问题,在那里我描述了3种运行变更检测的方法,其中包括detectChanges() - Mark Rajcok
4
请注意,当前问题中调用的方法名为updateMessage而不是setMessage - superjos
3
@Daynil,我也有同样的感觉,直到我阅读了在问题下方评论中提供的博客:https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4该博客解释了为什么需要手动完成这个操作。在这种情况下,Angular的变更检测具有生命周期。如果某些内容在这些生命周期之间更改了值,则需要强制运行变更检测(或者使用setTimeout - 它在下一个事件循环中执行,再次触发变更检测)。 - Mahesh
显示剩余10条评论

94

ngAfterViewChecked() 对我起了作用:

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

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

1
正如Drew所提到的,Angular的变更检测周期分为两个阶段,必须在子视图修改后检测到更改。我认为最好的方法是使用Angular自身提供的生命周期钩子,并要求Angular手动检测并绑定更改。我个人认为这似乎是一个合适的答案。 - vijayakumarpsg587
1
这对我来说可行,可以一起加载层次结构动态组件。 - Mehdi Daustany
1
唯一的问题是,每当您在屏幕上执行任何操作时,它都会运行。 - Kanchan Tyagi
1
以上解决方案对我有效。 - Kapil Soni

67

我通过从Angular核心添加ChangeDetectionStrategy来解决了这个问题。

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

这对我起作用了。我想知道使用它和使用ChangeDetectorRef的区别在哪里。 - Ari
1
嗯...那么变化检测器的模式将最初设置为CheckOnce文档)。 - Alex Klaus
是的,错误/警告消息已经消失了。但是它比以前加载时间要长得多,大约5-7秒的差异是非常巨大的。 - Kapil Raghuwanshi
1
@KapilRaghuwanshi 在你尝试加载任何内容后运行 this.cdr.detectChanges();。因为这可能是因为变更检测没有触发。 - Harshal Carpenter
不要使用changeDetection:ChangeDetectionStrategy.OnPush,这会阻止HTML和TS之间的生命周期。 - Mohammad Mehdi Mohammadi

54

如果你只是改变成员变量 message,为什么不能使用 ngOnInit

如果你想要访问子组件的引用 @ViewChild(ChildComponent),确实需要使用 ngAfterViewInit 等待它。

一种不太好的解决方法是使用 setTimeout 在下一个事件循环中调用 updateMessage()

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

5
将我的代码改为ngOnInit方法对我起了作用。 - Faliorn

49

对于这个问题,我已经尝试了以上许多答案,在最新版本的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
2
如果以上方法都不起作用,我建议采用这个答案。 - Amir Choubani
5
每次DOM重新计算或重新检查时,都会调用afterContentChecked。这是从Angular版本9开始推出的特性。 - Imran Faruqi
是的,https://angular.io/api/core/AfterContentChecked,你是对的,它是默认的在变更事件之后调用的方法。 - MarmiK

43

我从AfterViewInit切换到AfterContentChecked,这对我有用。

以下是步骤:

  1. 在你的构造函数中添加依赖项:

    constructor (private cdr: ChangeDetectorRef) {}

  2. 并在实现的方法中调用你的登录代码:

     ngAfterContentChecked() {
         this.cdr.detectChanges();
      // call or add here your code
     }
    

1
是的,这对我也起作用了。我正在使用AfterViewInit。 - abrsh
1
这个可行,我猜这应该是正确的答案。 - Silambarasan R.D
2
对我来说,使用 ngAfterContentChecked 在性能方面似乎不是明智的选择。在我的小样本中,即使在 Angular 完成视图初始化后滚动,代码也会被执行多次。对于性能有什么想法吗? - Smamatti

27

这篇文章详细解释了ExpressionChangedAfterItHasBeenCheckedError错误的行为.

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

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

而这将需要进行另一个变更检测周期,而Angular的设计只运行一次脏检查循环。

你基本上有两种选择来修复它:

  • 使用setTimeoutPromise.then或在模板中引用的异步observable异步更新属性

  • 在DOM更新之前执行属性更新 - ngOnInit、ngDoCheck、ngAfterContentInit、ngAfterContentChecked


1
阅读了你的文章: https://blog.angularindepth.com/a-gentle-introduction-into-change-detection-in-angular-33f9ffff6f10,很快会阅读另一篇 https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f。仍然无法解决这个问题。你能告诉我如果我添加ngDoCheck或ngAfterContentChecked生命周期钩子,并在其中添加this.cdr.markForCheck();(cdr为ChangeDetectorRef),会发生什么吗?在生命周期钩子和随后的检查完成后检查更改不是正确的方法吗? - Always_a_learner
读了你的文章,然后使用 Promise.then 解决了我的问题。顺便说一下,在调试时当我注释掉 enableProdMode(); 时出现了这个问题。在生产环境中没有发生,但创建一个微任务是有意义的。 - Davut Gürbüz

19

出现此错误是因为在初始化后立即更新了现有值。因此,如果您在现有值呈现在DOM后更新新值,则会正常工作。就像在这篇文章中提到的Angular调试 "Expression has changed after it was checked"

例如,您可以使用

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,因此它将在浏览器堆栈中单独运行,并等待调用堆栈项完成。

如何调用浏览器API的概念是由Philip Roberts在YouTube视频(What the hack is event loop?)中解释的。


请查看这个关于JS概念的最佳内容 -> https://www.youtube.com/watch?v=pN6jk0uUrD8&list=PLlasXeu85E9cQ32gLCvAvr9vNaUccPVNP - Suprabhat Kumar

13

您只需要在正确的生命周期钩子中更新您的消息,本例中为ngAfterContentChecked而不是ngAfterViewInit,因为在ngAfterViewInit中已经开始了对变量message的检查但尚未结束。

参见: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上查看演示


1
我使用了一个@ViewChildren()基于长度计数器,它由Observable填充。这是唯一对我有效的解决方案! - msanford
2
使用上述的 ngAfterContentCheckedChangeDetectorRef 的组合对我来说很管用。在 ngAfterContentChecked 中调用 - this.cdr.detectChanges(); - Kunal Dethe

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