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

时间:2015-12-18 22:28:42

标签: typescript angular

为什么这个组件很简单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 :)'
  }
}

投掷:

  

EXCEPTION:App @ 0:5'中的表达式'我是{{message}}在检查后发生了变化。上一个值:'我正在加载:('。当前值:'我已经完成了加载:)'在[我是{{message}}在App @ 0:5中]

什么时候我正在做的是在我的视图启动时更新一个简单的绑定?

18 个答案:

答案 0 :(得分:215)

正如drewmoore所述,在这种情况下,正确的解决方案是手动触发当前组件的变化检测。这是使用detectChanges()对象的ChangeDetectorRef方法(从angular2/core导入)或其markForCheck()方法完成的,该方法还会更新任何父组件。相关的example

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

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

  constructor(private cdr: ChangeDetectorRef) {}

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

}

以下是针对以下情况展示ngOnInitsetTimeoutenableProdMode方法的掠夺者。

答案 1 :(得分:134)

首先,请注意,只有在您以开发模式运行应用程序时才会抛出此异常(默认情况下属于beta-0):如果您在引导应用程序时调用enableProdMode(),它不会被抛出(see updated plunk)。

其次,不要这样做因为这个异常是有充分理由抛出的:简而言之,当处于开发模式时,每一轮变化检测都会立即跟进第二轮验证否自第一个结束以来,绑定已经发生了变化,因为这表明变化是由变化检测本身引起的。

在您的插件中,绑定{{message}}会因您对setMessage()的调用而更改,该调用发生在ngAfterViewInit挂钩中,这是初始更改检测转弯的一部分。这本身并没有问题 - 问题是setMessage()改变了绑定但没有触发新一轮的变化检测,这意味着在未来的一轮变化检测被触发之前不会检测到这种变化别的地方。

外卖:任何改变绑定的东西都需要触发一轮变更检测

更新以响应所有请求以获取如何执行该操作的示例:@ Tycho的解决方案正常工作,the answer @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 :)')
  }
}

答案 2 :(得分:37)

我通过从angular core添加ChangeDetectionStrategy来解决这个问题。

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

答案 3 :(得分:27)

您是否可以使用ngOnInit,因为您只是更改了成员变量message

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

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

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

答案 4 :(得分:24)

ngAfterViewChecked()为我工作:

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

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

答案 5 :(得分:19)

文章Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error详细解释了这种行为。

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

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

这将需要另一个更改检测周期,而Angular by design只运行一个摘要周期。

你基本上有两种选择来解决它:

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

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

答案 6 :(得分:13)

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

这是过程

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

    constructor (private cdr: ChangeDetectorRef) {}

  2. ,并在此处通过已实现的方法代码调用登录名:

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

答案 7 :(得分:6)

您只需在正确的生命周期钩子中更新您的消息,在这种情况下是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上的working demo

答案 8 :(得分:5)

为此,我尝试了以上答案,但在最新版本的Angular(6或更高版本)中,许多功能均无效

我正在使用“材质”控件,该控件需要在第一次绑定后进行更改。

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

添加我的答案,这样可以帮助解决某些特定问题。

答案 9 :(得分:4)

您也可以在ngOnInt()方法中调用updateMessage(),至少它适用于我

ngOnInit() {
    this.updateMessage();
}

在RC1中,这不会触发异常

答案 10 :(得分:2)

抛出错误是因为在调用 ngAfterViewInit()时代码会更新。意味着你的初始值在ngAfterViewInit发生时被改变了,如果你在 ngAfterContentInit()中调用它,那么它就不会抛出错误。

ngAfterContentInit() {
    this.updateMessage();
}

答案 11 :(得分:1)

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

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

答案 12 :(得分:1)

即将出现此错误是因为在初始化后立即更新了现有值。因此,如果您要在DOM中呈现现有值后更新新值,那么它将正常工作。就像本文Angular Debugging "Expression has changed after it was checked"

中提到的那样

例如,您可以使用

ngAfterViewInit() {
    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的调用方式(事件循环是什么?)。

答案 13 :(得分:1)

就我而言,它是通过p-radioButton发生的。问题是我在像这样的formControlName属性旁边使用了name属性(不需要):

<p-radioButton formControlName="isApplicant" name="isapplicant" value="T" label="Yes"></p-radioButton>
<p-radioButton formControlName="isApplicant" name="isapplicant" value="T" label="No"></p-radioButton>

我还具有绑定到isApplicant表单控件的初始值“ T”,如下所示:

isApplicant: ["T"]
通过删除单选按钮中的名称属性,

解决了问题。 另外,由于2个单选按钮的值(T)相同(在我的情况下是错误的),因此只需将其中一个更改为另一个值(例如F)也可以解决此问题。

答案 14 :(得分:1)

我有几乎相同的案例,我有一系列产品。我必须让用户根据他们的选择删除产品。最后,如果数组中没有产品,那么我需要在不重新加载页面的情况下显示取消按钮而不是后退按钮。

我通过在 ngAfterViewChecked() 生命周期钩子中检查空数组来完成它。 这就是我完成的方式,希望它有帮助:)

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

products: Product[];
someCondition: boolean;

constructor(private cdr: ChangeDetectorRef) {}

ngAfterViewChecked() {
  if(!this.someCondition) {
    this.emptyArray();
  }
}

emptyArray() {
    this.someCondition = this.products.length === 0 ? true : false;

    // run change detection explicitly
    this.cdr.detectChanges();
}

removeProduct(productId: number) {
    // your logic for removing product.
}

答案 15 :(得分:0)

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

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

这解决了这个问题,但我想知道这有什么副作用。

答案 16 :(得分:0)

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

答案 17 :(得分:0)

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

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