【问题标题】:Angular 4 - "Expression has changed after it was checked" error while using NG-IFAngular 4 - 使用 NG-IF 时出现“检查后表达式已更改”错误
【发布时间】:2026-01-04 01:25:02
【问题描述】:

我设置了一个service 来跟踪登录用户。该服务返回一个 Observable,并通知所有 subscribe 给它的组件(目前只有一个组件订阅它)

服务:

private subject = new Subject<any>();

sendMessage(message: boolean) {
   this.subject.next( message );
}

getMessage(): Observable<any> {
   return this.subject.asObservable();
} 

根应用组件:(此组件订阅可观察对象)

ngAfterViewInit(){
   this.subscription = this._authService.getMessage().subscribe(message => { this.user = message; });
}

欢迎组件:

ngOnInit() {
  const checkStatus = this._authService.checkUserStatus();
  this._authService.sendMessage(checkStatus);
}

App Component Html:(这是发生错误的地方)

<div *ngIf="user"><div>

我想做的事:

我希望每个组件(根应用程序组件除外)都将用户登录状态发送到根应用程序组件,以便我可以在根应用程序组件 Html 中操作 UI。

问题:

欢迎组件初始化时出现以下错误。

Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'true'.

请注意此错误发生在位于根应用程序组件 HTML 文件中的 *ngIf="user" 表达式上。

谁能解释这个错误的原因以及我该如何解决这个问题?

附带说明:如果您认为有更好的方法来实现我想要做的事情,请告诉我。

更新 1:

将以下内容放入constructor 可以解决问题,但不想为此目的使用constructor,因此这似乎不是一个好的解决方案。

欢迎组件:

constructor(private _authService: AuthenticationService) {
  const checkStatus = this._authService.checkUserStatus();
  this._authService.sendMessage(checkStatus);
 }

根应用组件:

constructor(private _authService: AuthenticationService){
   this.subscription = this._authService.getMessage().subscribe(message => { this.usr = message; });
}

更新 2:

这是plunkr。要查看错误,请检查浏览器控制台。当应用程序加载时,应该显示一个布尔值 true,但我在控制台中收到错误。

请注意,这个 plunkr 是我的主应用程序的一个非常基本的版本。由于应用程序有点大,我无法上传所有代码。但是 plunkr 完美地证明了这个错误。

【问题讨论】:

标签: javascript angular typescript rxjs


【解决方案1】:

这意味着变化检测周期本身似乎引起了变化,这可能是偶然的(即变化检测周期以某种方式引起的)或故意的。如果您确实在更改检测周期中故意更改了某些内容,那么这应该会重新触发新一轮的更改检测,而这里不会发生这种情况。此错误将在 prod 模式下被抑制,但这意味着您的代码中存在问题并导致神秘问题。

在这种情况下,具体问题是您正在更改影响父级的子级更改检测周期中的某些内容,即使像可观察对象这样的异步触发器通常会这样做,这也不会重新触发父级的更改检测。它不重新触发父循环的原因是因为这违反了单向数据流,并且可能会导致子重新触发父更改检测循环,然后重新触发子,然后再次触发父循环等等,并导致您的应用程序中的无限变化检测循环。

这听起来像是我在说孩子无法向父组件发送消息,但事实并非如此,问题是在更改检测期间孩子无法向父组件发送消息循环(例如生命周期钩子),它需要在外部发生,例如响应用户事件。

这里最好的解决方案是通过创建一个不是导致更新的组件的父级的新组件来停止违反单向数据流,这样就无法创建无限变化检测循环。这在下面的 plunkr 中得到了证明。

添加了子元素的新 app.component:

<div class="col-sm-8 col-sm-offset-2">
      <app-message></app-message>
      <router-outlet></router-outlet>
</div>

消息组件:

@Component({
  moduleId: module.id,
  selector: 'app-message',
  templateUrl: 'message.component.html'
})
export class MessageComponent implements OnInit {
   message$: Observable<any>;
   constructor(private messageService: MessageService) {

   }

   ngOnInit(){
      this.message$ = this.messageService.message$;
   }
}

模板:

<div *ngIf="message$ | async as message" class="alert alert-success">{{message}}</div>

稍微修改了消息服务(只是结构更简洁):

@Injectable()
export class MessageService {
    private subject = new Subject<any>();
    message$: Observable<any> = this.subject.asObservable();

    sendMessage(message: string) {
       console.log('send message');
        this.subject.next(message);
    }

    clearMessage() {
       this.subject.next();
    }
}

这比仅仅让变更检测正常工作而没有创建无限循环的风险有更多的好处。它还使您的代码更加模块化并更好地隔离了责任。

https://plnkr.co/edit/4Th7m0Liovfgd1Z3ECWh?p=preview

【讨论】:

  • 您好,谢谢您的回答。我试着把它放在ngOnInIt,但我仍然得到同样的错误。
  • 可能有一个奇怪的时间问题。异步管道方法将起作用。
  • 我尝试了异步管道方法,但仍然是同样的错误。虽然现在它说Previous value: '[object Object]'. Current value: 'true'.一定是我做错了什么。
  • 听起来好像还有其他事情发生,你需要做一个 plunkr 以确保没有你从这个样本中排除的东西导致问题,因为这应该可以正常工作。
  • 好的,我会做一个 plunkr 并用它更新我的问题。谢谢您的帮助!! :)
【解决方案2】:

在构造函数中声明这一行

private cd: ChangeDetectorRef

在这样的 ngAfterviewInit 调用之后

ngAfterViewInit() {
   // it must be last line
   this.cd.detectChanges();
}

它将解决您的问题,因为 DOM 元素布尔值不会改变。所以它的抛出异常

您的 Plunkr 答案请查看 AppComponent

import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { MessageService } from './_services/index';

@Component({
    moduleId: module.id,
    selector: 'app',
    templateUrl: 'app.component.html'
})

export class AppComponent implements OnDestroy, OnInit {
    message: any = false;
    subscription: Subscription;

    constructor(private messageService: MessageService,private cd: ChangeDetectorRef) {
        // subscribe to home component messages
        //this.subscription = this.messageService.getMessage().subscribe(message => { this.message = message; });
    }

    ngOnInit(){
      this.subscription = this.messageService.getMessage().subscribe(message =>{
         this.message = message
         console.log(this.message);
         this.cd.detectChanges();
      });
    }

    ngOnDestroy() {
        // unsubscribe to ensure no memory leaks
        this.subscription.unsubscribe();
    }
}

【讨论】:

  • 这是一个 hack,实际上并没有解决根本问题。如果您需要变更检测器,您几乎总是在做错事。
  • @bryan60 这根本不是 hack,你需要在这里扔文件,这是为了你的信息angular.io/api/core/ChangeDetectorRef
  • 另一个链接“关于 Angular 中的变更检测你需要知道的一切”@maxim_koretskyi blog.angularindepth.com/…
  • 这是一个 hack,当您的应用程序达到一定的大小和复杂性时,手动使用更改检测器会导致问题。它有效,但它是一个非常糟糕的解决方案,基本上相当于将你的函数包装在超时中。问题是他正在更改检测周期中的绑定,而不会触发另一轮更改检测。这是代码设计不佳的标志,并且像您提出的那样的变通方法是鼓励不良实践的坏主意。
  • 我更新的答案解释了问题所在,为什么他的做法是不好的做法,以及为什么不应该用变通方法修补不好的做法。 Angular 抛出这个错误是有原因的。
【解决方案3】:

很好的问题,那么,是什么导致了问题?这个错误的原因是什么?我们需要了解 Angular 变化检测是如何工作的,我将简要解释一下:

  • 将属性绑定到组件
  • 您运行一个应用程序
  • 发生事件(超时、ajax 调用、DOM 事件...)
  • 绑定的属性随着事件的影响而改变
  • Angular 也会监听事件并运行 Change Detection Round
  • Angular 更新视图
  • Angular 调用生命周期钩子 ngOnInitngOnChangesngDoCheck
  • Angular 在所有子组件中运行更改检测轮次
  • Angular 调用生命周期钩子ngAfterViewInit

但是,如果生命周期挂钩包含再次更改属性的代码,并且没有运行更改检测轮,该怎么办?或者,如果生命周期挂钩包含导致另一轮变更检测轮的代码并且代码进入循环怎么办?这是一种危险的可能性,Angular 会阻止它注意不要在此期间或之后立即更改的属性。这是通过在第一轮之后执行第二轮更改检测轮来实现的,以确保没有任何更改。注意:这只发生在开发模式下。

如果您同时触发两个事件(或在非常短的时间范围内),Angular 将同时触发两个更改检测周期,在这种情况下没有问题,因为 Angular 既然两个事件都触发了一个更改检测轮次,Angular 足够智能,可以了解正在发生的事情。

但并非所有事件都会导致变更检测轮次,您的就是一个例子:Observable 不会触发变更检测策略。

你要做的是唤醒 Angular 触发一轮变化检测。你可以使用EventEmitter,一个超时,whatever 导致一个事件。

我最喜欢的解决方案是使用window.setTimeout

this.subscription = this._authService.getMessage().subscribe(message => window.setTimeout(() => this.usr = message, 0));

这解决了问题。

【讨论】:

  • 与另一个提倡使用变更检测器的答案相同,这是一种解决方法,不能解决根本问题,即在变更检测周期中违反单向数据流。 Angular 抛出这个错误是有原因的,因为它可能会产生微妙且难以注意到的错误或无限变化检测循环。这种变通方法是一种不好的做法,应该避免。此外,通常与异步管道一起使用时,可观察对象会触发更改检测,这是一种特殊情况,因为 Angular 正在采取措施确保未创建更改检测循环
  • 我在 Angular 环境中多次看到这个解决方案,我不知道这是一个不好的做法。解决方案应该是什么样的?
  • 如果您正在使用更改检测器或设置超时或创建虚拟更改检测,那么您的做法是错误的。解决方案是创建一个组件,该组件不是在更改检测周期中进行更改的组件的父组件,这样就不会违反单向数据流。
【解决方案4】:

要了解错误,请阅读:

您的案例属于Synchronous event broadcasting 类别:

此模式在by this plunker 中进行了说明。该应用程序是 设计为有一个发出事件的子组件和一个父组件 监听此事件的组件。该事件导致一些父 要更新的属性。并且这些属性被用作输入 子组件的绑定。这也是间接父母 属性更新。

在您的情况下,更新的父组件属性是user,并且此属性用作*ngIf="user" 的输入绑定。问题是您正在触发一个事件 this._authService.sendMessage(checkStatus) 作为变更检测周期的一部分,因为您是从生命周期挂钩中进行的。

如文章中所述,您有两种解决此错误的一般方法:

  • 异步更新 - 这允许在更改检测过程之外触发事件
  • 强制变更检测 - 这会在当前运行和验证阶段之间添加额外的变更检测运行

首先,您必须回答是否需要从生命周期挂钩触发偶数。如果您在组件构造函数中拥有所需的所有信息,我认为这不是一个糟糕的选择。详情请见The essential difference between Constructor and ngOnInit in Angular

在您的情况下,我可能会使用异步事件触发而不是手动更改检测来避免冗余的更改检测周期:

ngOnInit() {
  const checkStatus = this._authService.checkUserStatus();
  Promise.resolve(null).then(() => this._authService.sendMessage(checkStatus););
}

或者在 AppComponent 内部进行异步事件处理:

ngAfterViewInit(){
    this.subscription = this._authService.getMessage().subscribe(Promise.resolve(null).then((value) => this.user = message));

ngModel in the implementation 使用了我上面展示的方法。

但我也想知道this._authService.checkUserStatus() 怎么是同步的?

【讨论】:

  • 这与其他2个答案完全相同,使用解决方法来触发另一轮更改检测。使用 Promise 与手动超时或更改检测器有何不同?它没有解决他违反单向数据流的根本问题。
  • @bryan60,有时您无法重新设计应用程序,如果您知道自己在做什么以及为什么要使用任何解决方案(异步更新、手动 cd),那么在这些情况下使用任何解决方案都可以。例如,Angular 本身在ngModel 来源中使用了这种方法。
  • 我不相信有一个用例你不能重新设计以不违反单向数据流,这种情况绝对不是其中之一。但是,是的,如果您出于非常特定的原因这样做一次,或者您正在尝试做一些非常不寻常的事情,那么解决方法是可以的。问题是,如果你不解决你的根本问题,它永远不会只是一次。随着应用程序的增长,问题变得更加复杂,直到最终完全无法使用。
  • Re angular 使用变更检测,是的,我希望框架启动变更检测,这就是框架的目的,将这个过程从应用程序开发人员那里抽象出来。这显然与开发人员手动启动变更检测有很大不同。
  • @bryan60,我不相信有一个用例你不能重新设计以不违反单向数据流 - 请参阅Dynamic component instantiation case here
【解决方案5】:

我最近在迁移到 Angular 4.x 后遇到了同样的问题,一个快速的解决方案是将导致 ChangeDetection 的代码的每个部分包装在 setTimeout(() =&gt; {}, 0) // notice the 0 it's intentional 中。

这样它将在life-cycle hook 之后推送emit,因此不会导致更改检测错误。 虽然我知道这是一个非常肮脏的解决方案,但它是一个可行的快速修复

【讨论】:

    【解决方案6】:

    不要改变ngOnInit中的var,在构造函数中改变它

    constructor(private apiService: ApiService) {
     this.apiService.navbarVisible(false);
    }
    

    【讨论】:

      最近更新 更多