webentwicklung-frage-antwort-db.com.de

Winkel 4 - Fehler "Ausdruck hat sich geändert, nachdem er geprüft wurde" bei Verwendung von NG-IF

Ich habe eine service eingerichtet, um die angemeldeten Benutzer zu verfolgen. Dieser Dienst gibt ein Observable zurück, und alle Komponenten, die subscribe an ihn gemeldet werden, werden benachrichtigt (bisher wird nur eine einzelne Komponente abonniert).

Bedienung:

private subject = new Subject<any>();

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

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

Root App Component: (diese Komponente abonniert das Beobachtbare)

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

Begrüßungskomponente: 

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

App Component Html: (hier tritt der Fehler auf)

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

Was ich versuche zu tun:

Ich möchte, dass jede Komponente (mit Ausnahme der Root-App-Komponente) den angemeldeten Status der Benutzer an die Root-App-Komponente sendet, sodass ich die Benutzeroberfläche innerhalb der Root-App-Komponenten-HTML bearbeiten kann.

Die Angelegenheit:

Ich erhalte die folgende Fehlermeldung, wenn die Welcome Component initialisiert wird.

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

Beachten Sie, dass dieser Fehler bei diesem *ngIf="user"-Ausdruck auftritt, der sich in der HTML-Datei der Stammanwendungskomponenten befindet. 

Kann jemand den Grund für diesen Fehler erklären und wie ich das beheben kann?

Eine Randbemerkung: Wenn Sie meinen, dass es einen besseren Weg gibt, um das zu erreichen, was ich versuche, dann lassen Sie es mich bitte wissen.

Update 1:

Wenn Sie Folgendes in die Variable constructor einfügen, wird das Problem gelöst, Sie möchten jedoch nicht die Variable constructor für diesen Zweck verwenden. Es scheint also, dass es keine gute Lösung ist.

Begrüßungskomponente:

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

Root-App-Komponente:

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

Update 2:

Hier ist der plunkr . Um den Fehler anzuzeigen, überprüfen Sie die Browserkonsole. Wenn die App geladen wird, sollte ein boolescher Wert von true angezeigt werden. Ich erhalte jedoch die Fehlermeldung in der Konsole.

Bitte beachten Sie, dass dieser Plan eine sehr einfache Version meiner Haupt-App ist. Da die App etwas groß ist, konnte ich den gesamten Code nicht hochladen. Aber der Plunkr demonstriert den Fehler perfekt.

13
Skywalker

Dies bedeutet, dass der Änderungserkennungszyklus selbst scheinbar eine Änderung verursacht hat, die möglicherweise zufällig war (dh der Änderungserkennungszyklus verursachte sie irgendwie) oder beabsichtigt war. Wenn Sie in einem Änderungserkennungszyklus absichtlich etwas ändern, sollte dies eine neue Änderungsrunde auslösen, die hier nicht stattfindet. Dieser Fehler wird im Prod-Modus unterdrückt, bedeutet jedoch, dass Sie Probleme mit Ihrem Code haben und mysteriöse Probleme verursachen. 

In diesem Fall besteht das spezifische Problem darin, dass Sie im Änderungserkennungszyklus eines Kindes etwas ändern, das sich auf das übergeordnete Element auswirkt, und dies wird die Änderungserkennung des übergeordneten Elements nicht erneut auslösen, auch wenn asynchrone Auslöser wie Observables dies normalerweise tun. Der Grund, warum der Elternzyklus nicht erneut ausgelöst wird, liegt darin, dass dadurch der unidirektionale Datenfluss verletzt wird. Dies könnte zu Situationen führen, in denen ein Kind einen Elternänderungserkennungszyklus erneut auslöst, der dann das Kind und dann wieder das Elternteil und so weiter auslöst eine unendliche Änderungserkennungsschleife in Ihrer App. 

Es könnte sich so anhören, als würde ich sagen, dass ein Kind keine Nachrichten an eine übergeordnete Komponente senden kann. Dies ist jedoch nicht der Fall. Das Problem ist, dass ein Kind während eines Änderungserkennungszyklus keine Nachricht an einen Elternteil senden kann (z (als Lebenszyklus-Hooks) muss es draußen geschehen, wie in Reaktion auf ein Benutzerereignis.

Die beste Lösung besteht darin, den unidirektionalen Datenfluss nicht zu verletzen, indem eine neue Komponente erstellt wird, die keine übergeordnete Komponente der Komponente ist, die die Aktualisierung verursacht, sodass keine Endlosänderungserkennungsschleife erstellt werden kann. Dies wird im nachstehenden Diagramm gezeigt.

Neue app.component mit Kind hinzugefügt:

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

nachrichtenkomponente:

@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$;
   }
}

vorlage:

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

etwas modifizierter Nachrichtendienst (nur eine etwas sauberere Struktur):

@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();
    }
}

Dies hat mehr Vorteile, als nur die Änderungserkennung ordnungsgemäß arbeiten zu lassen, ohne dass das Risiko besteht, unendliche Schleifen zu erzeugen. Es macht Ihren Code auch modularer und isoliert die Verantwortung besser.

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

12
bryan60

Schöne Frage, also, was verursacht das Problem? Was ist der Grund für diesen Fehler? Wir müssen verstehen, wie die Erkennung von Winkeländerungen funktioniert. Ich werde es kurz erklären:

  • Sie binden eine Eigenschaft an eine Komponente
  • Sie führen eine Anwendung aus
  • Ein Ereignis tritt auf (Timeouts, Ajax-Aufrufe, DOM-Ereignisse, ...).
  • Die gebundene Eigenschaft wird als ein Effekt des Ereignisses geändert
  • Angular hört auch auf das Ereignis und führt eine Change Detection Round aus.
  • Angular aktualisiert die Ansicht
  • Angular ruft die Lebenszyklus-Hooks ngOnInit, ngOnChanges und ngDoCheck auf.
  • Führen Sie in allen untergeordneten Komponenten ein Change Detection Round aus
  • Angular ruft die Lebenszyklus-Hooks auf ngAfterViewInit

Was aber, wenn ein Lebenszyklus-Hook einen Code enthält, der die Eigenschaft erneut ändert, und ein Change Detection Round nicht ausgeführt wird? Oder was ist, wenn ein Lebenszyklus-Hook einen Code enthält, der eine weitere Change Detection Round auslöst und der Code in eine Schleife eintritt? Dies ist eine gefährliche Eventualität. Angular verhindert, dass sich die Eigenschaft in der Zwischenzeit oder unmittelbar danach ändert. Dies wird durch Ausführen einer zweiten Change Detection Round nach der ersten erreicht, um sicherzustellen, dass nichts geändert wird. Achtung: Dies passiert nur im Entwicklungsmodus.

Wenn Sie zwei Ereignisse gleichzeitig (oder in einem sehr kurzen Zeitraum) auslösen, löst Angular gleichzeitig zwei Änderungserkennungszyklen aus. In diesem Fall treten keine Probleme auf, da Angular beide Ereignisse ein Änderungserkennungsrunde und Angular ist intelligent genug, um zu verstehen, was passiert.

Aber nicht alle Ereignisse verursachen eine Change Detection Round, und Ihre ist ein Beispiel: Ein Observable löst keine Strategie zur Änderungserkennung aus.

Was Sie tun müssen, ist, dass Sie Angular wecken und eine Änderungsrunde auslösen. Sie können eine EventEmitter, ein Timeout, was auch immer ein Ereignis auslösen.

Meine Lieblingslösung ist window.setTimeout:

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

Das löst das Problem. 

2

Deklarieren Sie diese Zeile im Konstruktor 

private cd: ChangeDetectorRef

danach in ngAfterviewInit so aufrufen

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

das Problem wird dadurch behoben, dass der boolesche Wert des DOM-Elements nicht geändert wird. also seine Wurfausnahme

Ihre Plunkr Antwort hier Bitte überprüfen Sie mit 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();
    }
}
1
Piyush Patel

Um den Fehler zu verstehen, lesen Sie:

Ihr Fall fällt in die Kategorie Synchronous event broadcasting:

Dieses Muster wird von diesem Plunker veranschaulicht. Die Anwendung ist entworfen, um eine untergeordnete Komponente zu haben, die ein Ereignis und ein übergeordnetes Element ausgibt Komponente, die auf dieses Ereignis hört. Das Ereignis verursacht einige der übergeordneten Elemente zu aktualisierende Eigenschaften. Diese Eigenschaften werden als Eingabe verwendet verbindlich für die untergeordnete Komponente. Dies ist auch ein indirektes übergeordnetes Element Eigenschaftsaktualisierung.

In Ihrem Fall ist die übergeordnete Komponenteneigenschaft, die aktualisiert wird, user, und diese Eigenschaft wird als Eingabebindung für *ngIf="user" verwendet. Das Problem ist, dass Sie ein Ereignis this._authService.sendMessage(checkStatus) als Teil des Änderungserkennungszyklus auslösen, da Sie es vom Lebenszyklus-Hook aus durchführen.

Wie in dem Artikel erläutert, gibt es zwei allgemeine Ansätze, um diesen Fehler zu umgehen:

  • Asynchronous update - ermöglicht das Auslösen eines Ereignisses außerhalb des Änderungserkennungsprozesses
  • Erzwingen der Änderungserkennung - fügt zusätzliche Änderungserkennungsläufe zwischen dem aktuellen Lauf und der Überprüfungsphase hinzu

Zuerst müssen Sie die Frage beantworten, ob es erforderlich ist, das Signal vom Lebenszyklus-Hook aus zu lösen. Wenn Sie im Komponentenkonstruktor über alle Informationen verfügen, die Sie auch für den Komponentenkonstruktor benötigen, glaube ich nicht, dass dies die schlechte Option ist. Siehe Der wesentliche Unterschied zwischen Konstruktor und ngOnInit in Angular für weitere Details.

In Ihrem Fall würde ich wahrscheinlich entweder eine asynchrone Ereignisauslösung anstelle der manuellen Änderungserkennung durchführen, um redundante Änderungserkennungszyklen zu vermeiden:

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

oder mit asynchroner Ereignisverarbeitung in der AppComponent:

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

Der oben gezeigte Ansatz wird von ngModel in der Implementierung verwendet.

Aber ich frage mich auch, wie kommt es, dass this._authService.checkUserStatus() synchron ist?

Ändern Sie nicht die Variable in ngOnInit, sondern ändern Sie sie im Konstruktor 

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

Ich bin vor kurzem nach der Migration auf Angular 4.x auf dasselbe Problem gestoßen. Eine schnelle Lösung besteht darin, jeden Teil des Codes einzuwickeln, der die ChangeDetection in setTimeout(() => {}, 0) // notice the 0 it's intentional verursacht.

Auf diese Weise wird emit NACH dem life-cycle hook verschoben, so dass kein Änderungserkennungsfehler auftritt. Obwohl mir bewusst ist, dass dies eine ziemlich schmutzige Lösung ist, ist es ein praktikabler Quickfix.