Ich habe etwas über diesen Fehler gelesen und nachgeforscht, aber ich bin mir nicht sicher, was die richtige Antwort für meine Situation ist. Ich verstehe, dass die Änderungserkennung im Dev-Modus zweimal ausgeführt wird. Ich möchte jedoch nur enableProdMode()
verwenden, um das Problem zu maskieren.
Hier ist ein einfaches Beispiel, bei dem die Anzahl der Zellen in der Tabelle mit zunehmender Breite des div ansteigen sollte. (Beachten Sie, dass die Breite des div nicht nur von der Bildschirmbreite abhängt, sodass @Media nicht einfach angewendet werden kann.)
Mein HTML sieht folgendermaßen aus (widget.template.html):
<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
<td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>
Das allein tut nichts. Ich vermute, das liegt daran, dass nichts die Änderungserkennung verursacht. Wenn ich jedoch die erste Zeile in die folgende Zeile ändere und eine leere Funktion zum Empfangen des Anrufs erstelle, beginnt sie zu arbeiten, aber gelegentlich bekomme ich den Ausdruck "Ausdruck hat sich geändert, nachdem der Fehler geprüft wurde".
<div #widgetParentDiv class="Content">
gets replaced with
<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
Meine beste Vermutung ist, dass mit dieser Änderung die Änderungserkennung ausgelöst wird und alles beginnt zu reagieren. Wenn sich jedoch die Breite schnell ändert, wird die Ausnahme ausgelöst, da die vorherige Iteration der Änderungserkennung länger als die Änderung der Breite des div dauerte.
Weitere Einzelheiten zu meinem Projekt finden Sie unter this ähnliche Frage.
Vielen Dank
Um Ihr Problem zu beheben, müssen Sie lediglich die Größe von div
in einer Komponenteneigenschaft nach jedem Größenänderungsereignis abrufen und speichern und diese Eigenschaft in der Vorlage verwenden. Auf diese Weise bleibt der Wert konstant, wenn die zweite Runde der Änderungserkennung im Dev-Modus ausgeführt wird.
Ich empfehle auch, @HostListener
Zu verwenden, anstatt (window:resize)
Zu Ihrer Vorlage hinzuzufügen. Wir verwenden @ViewChild
, Um einen Verweis auf das div
zu erhalten. Und wir werden den Lebenszyklus-Hook ngAfterViewInit()
verwenden, um den Anfangswert festzulegen.
import {Component, ViewChild, HostListener} from '@angular/core';
@Component({
selector: 'my-app',
template: `<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="divWidth > 350">Value2</td>
<td *ngIf="divWidth > 700">Value3</td>
</tr>
</table>`,
})
export class AppComponent {
divWidth = 0;
@ViewChild('widgetParentDiv') parentDiv:ElementRef;
@HostListener('window:resize') onResize() {
// guard against resize before view is rendered
if(this.parentDiv) {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
ngAfterViewInit() {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
Schade, dass das nicht funktioniert. Wir bekommen
Der Ausdruck hat sich geändert, nachdem er überprüft wurde. Vorheriger Wert: 'false'. Aktueller Wert: 'true'.
Der Fehler beklagt sich über unsere NgIf
Ausdrücke - beim ersten Ausführen ist divWidth
gleich 0, dann wird ngAfterViewInit()
ausgeführt und der Wert wird in einen anderen Wert als 0 geändert Die 2. Runde der Änderungserkennung wird ausgeführt (im Dev-Modus). Zum Glück gibt es eine einfache/bekannte Lösung, und dies ist ein einmaliges Problem, kein fortbestehendes Problem wie im OP:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
Beachten Sie, dass diese Technik des Wartens auf einen Tick hier dokumentiert ist: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child =
In ngAfterViewInit()
und ngAfterViewChecked()
müssen wir häufig den Trick setTimeout()
anwenden, da diese Methoden aufgerufen werden, nachdem die Ansicht der Komponente erstellt wurde.
Hier ist eine Arbeit plunker.
Wir können das verbessern. Ich denke, wir sollten die Größenänderungsereignisse so drosseln, dass die Angular Änderungserkennung nur etwa alle 100-250 ms ausgeführt wird und nicht jedes Mal, wenn ein Größenänderungsereignis auftritt. Dies sollte verhindern, dass die App in diesem Fall träge wird Der Benutzer ändert die Größe des Fensters, da derzeit bei jedem Größenänderungsereignis die Änderungserkennung ausgeführt wird (zweimal im Dev-Modus). Sie können dies überprüfen, indem Sie dem vorherigen Plunker die folgende Methode hinzufügen:
ngDoCheck() {
console.log('change detection');
}
Observables können Ereignisse leicht drosseln. Anstatt @HostListener
Zum Binden an das Größenänderungsereignis zu verwenden, erstellen wir eine Observable:
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
Dies funktioniert, aber ... während ich damit experimentierte, entdeckte ich etwas sehr Interessantes ... obwohl wir das Größenänderungsereignis drosseln, wird die Angular Änderungserkennung immer dann ausgeführt, wenn es ein Größenänderungsereignis gibt. Das heißt, die Drosselung hat keinen Einfluss darauf, wie oft die Änderungserkennung ausgeführt wird. (Tobias Bosch hat dies bestätigt: https://github.com/angular/angular/issues/1773#issuecomment-10207825 .)
Ich möchte, dass die Änderungserkennung nur ausgeführt wird, wenn das Ereignis die Drosselungszeit überschreitet. Und ich brauche nur Änderungserkennung, um auf dieser Komponente ausgeführt zu werden. Die Lösung besteht darin, das Observable außerhalb der Angular Zone zu erstellen und dann die Änderungserkennung innerhalb des Abonnement-Rückrufs manuell aufzurufen:
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
// set initial value, but wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
this.ngzone.runOutsideAngular( () =>
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
this.cdref.detectChanges();
})
);
}
Hier ist eine Arbeit plunker.
Im Plunker habe ich ein counter
hinzugefügt, das ich mit dem Lifecycle-Hook ngDoCheck()
inkrementiere. Sie können sehen, dass diese Methode nicht aufgerufen wird - der Zählerwert ändert sich bei Ereignissen zur Größenänderung nicht.
detectChanges()
führt die Änderungserkennung für diese Komponente und ihre untergeordneten Komponenten aus. Wenn Sie die Änderungserkennung lieber über die Root-Komponente ausführen möchten (d. H. Eine vollständige Änderungserkennung durchführen), verwenden Sie stattdessen ApplicationRef.tick()
(dies ist im Plunker auskommentiert). Beachten Sie, dass tick()
bewirkt, dass ngDoCheck()
aufgerufen wird.
Das ist eine gute Frage. Ich habe viel Zeit damit verbracht, verschiedene Lösungen auszuprobieren und viel gelernt. Vielen Dank, dass Sie diese Frage gestellt haben.
Auf andere Weise habe ich das gelöst:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'your-seelctor',
template: 'your-template',
})
export class YourComponent{
constructor(public cdRef:ChangeDetectorRef) { }
ngAfterViewInit() {
this.cdRef.detectChanges();
}
}
Die beste Lösung ist, setTimeout oder delay für die Dienste zu verwenden.
Verwenden Sie einfach
setTimeout(() => {
//Your expression to change if state
});