【问题标题】:Expression ___ has changed after it was checked表达式___在检查后发生了变化
【发布时间】:2017-12-27 03:10:37
【问题描述】:

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

投掷:

例外:表达式 'I'm {{message}} in App@0:5' 在检查后已更改。上一个值:'我正在加载:('。当前值:'我已经完成加载:)' in [I'm {{message}} in App@0:5]

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

【问题讨论】:

标签: typescript angular


【解决方案1】:

首先,请注意,仅当您在开发模式下运行您的应用程序时才会抛出此异常(从 beta-0 开始默认为这种情况):如果您在引导应用程序时调用 enableProdMode(),它会赢不要被抛出 (see updated plunk)。

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

在您的 plunk 中,绑定 {{message}} 会通过您对 setMessage() 的调用进行更改,这发生在 ngAfterViewInit 挂钩中,该挂钩是初始更改检测轮次的一部分。这本身并没有问题 - 问题是setMessage() 更改了绑定但不会触发新一轮的更改检测,这意味着在其他地方触发未来一轮的更改检测之前不会检测到此更改.

要点:任何更改绑定都需要触发一轮更改检测

更新以响应所有关于如何做到这一点的示例的请求:@Tycho 的解决方案有效,正如the answer@MarkRajcok 中指出的三种方法一样。但坦率地说,它们都让我觉得丑陋和错误,就像我们习惯在 ng1 中依赖的那种 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 :)')
  }
}

【讨论】:

  • 为什么setMessage()没有触发新一轮的变更检测?我认为当您更改 UI 中某些内容的值时,Angular 2 会自动触发更改检测。
  • @drewmoore “任何改变绑定的东西都需要触发一轮变化检测”。如何?这是一个好习惯吗?不应该一次完成所有事情吗?
  • @Tycho,确实如此。自从我写了那条评论,我已经回答了另一个问题,我描述了3 ways to run change detection,其中包括detectChanges()
  • 请注意,在当前问题正文中,调用的方法名为updateMessage,而不是setMessage
  • @Daynil,我也有同样的感觉,直到我阅读了问题下评论中给出的博客:blog.angularindepth.com/… 它解释了为什么需要手动完成。在这种情况下,Angular 的变更检测具有生命周期。如果某些东西在这些生命周期之间更改了值,则需要运行强制更改检测(或 settimeout - 在下一个事件循环中执行,再次触发更改检测)。
【解决方案2】:

你不能使用ngOnInit,因为你只是改变了成员变量message

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

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

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

【讨论】:

  • 将我的代码更改为 ngOnInit 方法对我有用。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

    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 演示 ngOnInitsetTimeoutenableProdMode 方法以防万一。

    【讨论】:

    • 就我而言,我正在打开一个模式。打开模式后,它显示消息“表达式___在检查后已更改”,所以我的解决方案添加了 this.cdr.detectChanges();打开我的模态后。谢谢!
    • 您的 cdr 属性声明在哪里?我希望在message 声明下看到类似cdr : any 的行或类似的行。只是担心我错过了什么?
    • @CodeCabbie 它在构造函数参数中。
    • 这个解决方案解决了我的问题!非常感谢!非常清晰和简单的方法。
    • 这对我有帮助 - 做了一些修改。我必须对通过 ngFor 循环生成的 li 元素进行样式设置。我需要在单击“排序”时根据 innerText 更改列表项中跨度的颜色,该排序更新了用作排序管道参数的布尔值(排序结果是数据的副本,因此样式没有得到仅使用 ngStyle 更新)。 - 我没有使用“AfterViewInit”,而是使用了'AfterViewChecked' - 我还确保importimplement AfterViewChecked。 注意:将管道设置为“纯:假”不起作用,我必须添加这个额外的步骤(:
    【解决方案5】:

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

    ngOnInit() {
        this.updateMessage();
    }
    

    在 RC1 中这不会触发异常

    【讨论】:

      【解决方案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

      【讨论】:

      • 我使用了由 Observable 填充的 @ViewChildren() length-based counter 的组合。这是唯一对我有用的解决方案!
      • 以上ngAfterContentCheckedChangeDetectorRef 的组合对我有用。在 ngAfterContentChecked 上调用 - this.cdr.detectChanges();
      【解决方案7】:

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

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

      【讨论】:

      • 这对我有用。我想知道这和使用 ChangeDetectorRef 有什么区别
      • Hmm... 那么变化检测器的模式将初始设置为CheckOnce (documentation)
      • 是的,错误/警告消失了。但是它比以前花费了更多的加载时间,比如 5-7 秒的差异,这是巨大的。
      • @KapilRaghuwanshi 在您尝试加载任何内容之后运行this.cdr.detectChanges();。因为这可能是因为没有触发变更检测
      • 不要使用changeDetection: ChangeDetectionStrategy.OnPush,这将阻止html和ts之间的生命周期
      【解决方案8】:

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

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

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

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

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

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

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

      【讨论】:

      • 阅读您的文章:blog.angularindepth.com/…,很快将阅读另一篇 blog.angularindepth.com/…。仍然无法找到解决此问题的方法。你能告诉我如果我添加 ngDoCheck 或 ngAfterContentChecked 生命周期钩子并添加 this.cdr.markForCheck(); 会发生什么吗? (cdr for ChangeDetectorRef) 在里面。这不是在生命周期挂钩和后续检查完成后检查更改的正确方法吗?
      • 阅读你的文章和 Promise.then 解决了我的问题。顺便说一句,当我注释掉 enableProdMode(); 时,它就发生了,我的意思是在调试时。 NG6,在生产中它没有发生,但创建一个微任务是有意义的..
      【解决方案9】:

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

      ngAfterContentInit() {
          this.updateMessage();
      }
      

      【讨论】:

        【解决方案10】:

        ngAfterViewChecked() 为我工作:

        import { Component, ChangeDetectorRef } from '@angular/core'; //import ChangeDetectorRef
        
        constructor(private cdr: ChangeDetectorRef) { }
        ngAfterViewChecked(){
           //your code to update the model
           this.cdr.detectChanges();
        }
        

        【讨论】:

        • 如图所示,angular的变化检测周期是两个阶段,必须在孩子的视图被修改后检测变化,我觉得最好的方法是使用提供的生命周期钩子angular 本身,并要求 angular 手动检测更改并绑定它。我个人认为这似乎是一个合适的答案。
        • 这对我有用,用于一起加载层次结构动态组件。
        【解决方案11】:

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

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

        这修复了错误,但我想知道这有什么副作用。

        【讨论】:

          【解决方案12】:

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

          我正在使用在首次绑定完成后需要更改的材料控制。

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

          添加我的答案,这有助于解决特定问题。

          【讨论】:

          • 这实际上适用于我的情况,但你有一个错字,在实施 AfterContentChecked 之后,你应该调用 ngAfterContentChecked,而不是 ngAfterViewInit。
          • 我目前使用的是 8.2.0 版本 :)
          • 如果以上都不起作用,我推荐这个答案。
          • afterContentChecked 每次重新计算或重新检查 DOM 时都会调用。来自 Angular 9 的提示
          • 是的angular.io/api/core/AfterContentChecked你是对的,更改事件后的默认方法
          【解决方案13】:

          出现此错误是因为现有值在初始化后立即更新。所以如果你在现有值在DOM中渲染后更新新值,那么它会正常工作。就像这篇文章中提到的Angular Debugging "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,所以这将在浏览器堆栈中单独运行,并等待调用堆栈项完成。

          Philip Roberts 在一段 Youtube 视频中解释了浏览器 API 如何引发概念(What the hack is event loop?)。

          【讨论】:

            【解决方案14】:

            我在使用数据表时遇到了类似的错误。当您在另一个 *ngFor 数据表中使用 *ngFor 时会发生这种情况,因为它会影响角度变化周期。所以不要在数据表中使用数据表,而是使用一个常规表或将 mf.data 替换为数组名称。这很好用。

            【讨论】:

              【解决方案15】:

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

              1. 执行一种为某个变量赋值的实现,即通过函数或 setter。
              2. 在该函数所在的类中创建一个类变量(static working: boolean),每次调用该函数时,只要你喜欢就让它为真。在函数内部,如果 working 的值为 true,则直接返回而不做任何事情。否则,执行您想要的任务。确保在任务完成后将此变量更改为 false,即在代码行的末尾或完成赋值后的 subscribe 方法中!

              【讨论】:

                【解决方案16】:

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

                流程如下

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

                  constructor (private cdr: ChangeDetectorRef) {}

                2. 并在此处调用您的登录实现方法代码:

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

                【讨论】:

                • 是的,这对我也有用。我正在使用 AfterViewInit。
                • 这行得通,我想这应该是正确的答案。
                • 对我来说,使用ngAfterContentChecked 似乎并不是关于性能的明智之举。在我的小示例中,代码将被执行多次,即使您在 Angular 完成视图初始化后滚动。对性能有什么想法吗?
                【解决方案17】:

                在我的例子中,它发生在一个 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>
                

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

                isApplicant: ["T"]
                

                我通过删除单选按钮中的名称属性修复了该问题。 此外,由于 2 个单选按钮具有相同的值 (T),这在我的情况下是错误的,只需将其中一个更改为另一个值(例如 F)也可以解决问题。

                【讨论】:

                  【解决方案18】:

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

                  我通过检查 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.
                  }
                  

                  【讨论】:

                    猜你喜欢
                    • 2019-01-05
                    • 1970-01-01
                    • 2018-11-17
                    • 2017-01-20
                    • 1970-01-01
                    • 2019-05-29
                    • 2021-04-22
                    相关资源
                    最近更新 更多