NgFor aktualisiert keine Daten mit Pipe in Angular2

113

In diesem Szenario zeige ich der Ansicht eine Liste der Schüler (Array) mit folgenden Eigenschaften an ngFor:

<li *ngFor="#student of students">{{student.name}}</li>

Es ist wunderbar, dass es aktualisiert wird, wenn ich einen anderen Schüler zur Liste hinzufüge.

Wenn ich es pipejedoch filtermit dem Namen des Schülers gebe ,

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

Die Liste wird erst aktualisiert, wenn ich etwas in das Feld zum Filtern des Schülernamens eingebe.

Hier ist ein Link zu plnkr .

Hello_world.html

<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
    <li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>

sort_by_name_pipe.ts

import {Pipe} from 'angular2/core';

@Pipe({
    name: 'sortByName'
})
export class SortByNamePipe {

    transform(value, [queryString]) {
        // console.log(value, queryString);
        return value.filter((student) => new RegExp(queryString).test(student.name))
        // return value;
    }
}

Chu Sohn
quelle
14
Fügen Sie pure:falsein Ihrem Rohr und changeDetection: ChangeDetectionStrategy.OnPushin Ihrer Komponente hinzu.
Eric Martinez
2
Danke @EricMartinez. Es klappt. Aber können Sie ein wenig erklären?
Chu Son
2
Außerdem würde ich empfehlen, NICHT .test()in Ihrer Filterfunktion zu verwenden. Dies liegt daran, dass Ihr Code unterbrochen wird , wenn der Benutzer eine Zeichenfolge eingibt, die Sonderzeichen wie: *oder +usw. enthält. Ich denke, Sie sollten .includes()Abfragezeichenfolge mit benutzerdefinierten Funktion verwenden oder entkommen.
Eggy
6
Durch Hinzufügen pure:falseund Bereitstellen Ihrer Pipe wird das Problem behoben. Das Ändern von ChangeDetectionStrategy ist nicht erforderlich.
Pixelbits
2
Für jeden, der dies liest, ist die Dokumentation für Angular Pipes viel besser geworden und geht auf viele der hier diskutierten Dinge ein. Hör zu.
0xcaff

Antworten:

153

Um das Problem und mögliche Lösungen vollständig zu verstehen, müssen wir die Erkennung von Winkeländerungen diskutieren - für Rohre und Komponenten.

Rohrwechselerkennung

Staatenlose / reine Rohre

Standardmäßig sind Pipes zustandslos / rein. Zustandslose / reine Pipes wandeln einfach Eingabedaten in Ausgabedaten um. Sie erinnern sich an nichts, haben also keine Eigenschaften - nur eine transform()Methode. Angular kann daher die Behandlung von zustandslosen / reinen Rohren optimieren: Wenn sich ihre Eingänge nicht ändern, müssen die Rohre während eines Änderungserkennungszyklus nicht ausgeführt werden. Für ein Rohr , wie beispielsweise {{power | exponentialStrength: factor}}, powerund factorsind Eingänge.

Für diese Frage "#student of students | sortByName:queryElem.value", studentsund queryElem.valuesind Eingänge, und das Rohr sortByNameist zustandslos / rein. studentsist ein Array (Referenz).

  • Wenn ein Schüler hinzugefügt wird, ändert sich die Array- Referenz nicht - studentsändert sich nicht - daher wird die zustandslose / reine Pipe nicht ausgeführt.
  • Wenn etwas in den Filtereingang eingegeben wird, queryElem.valueändert sich dies, sodass die zustandslose / reine Pipe ausgeführt wird.

Eine Möglichkeit, das Array-Problem zu beheben, besteht darin, die Array-Referenz jedes Mal zu ändern, wenn ein Schüler hinzugefügt wird, dh jedes Mal, wenn ein Schüler hinzugefügt wird, ein neues Array zu erstellen. Wir könnten dies tun mit concat():

this.students = this.students.concat([{name: studentName}]);

Obwohl dies funktioniert, sollte unsere addNewStudent()Methode nicht auf eine bestimmte Weise implementiert werden müssen, nur weil wir eine Pipe verwenden. Wir möchten verwenden push(), um unser Array zu erweitern.

Stateful Pipes

Stateful Pipes haben State - sie haben normalerweise Eigenschaften, nicht nur eine transform()Methode. Sie müssen möglicherweise ausgewertet werden, auch wenn sich ihre Eingaben nicht geändert haben. Wenn wir angeben, dass eine Pipe zustandsbehaftet / nicht rein ist - pure: false-, prüft das Änderungserkennungssystem von Angular, wenn eine Komponente auf Änderungen überprüft und diese Komponente eine zustandsbehaftete Pipe verwendet, die Ausgabe der Pipe, ob sich ihre Eingabe geändert hat oder nicht.

Dies klingt nach dem, was wir wollen, obwohl es weniger effizient ist, da die Pipe auch dann ausgeführt werden soll, wenn sich die studentsReferenz nicht geändert hat. Wenn wir die Pipe einfach zustandsbehaftet machen, erhalten wir eine Fehlermeldung:

EXCEPTION: Expression 'students | sortByName:queryElem.value  in HelloWorld@7:6' 
has changed after it was checked. Previous value: '[object Object],[object Object]'. 
Current value: '[object Object],[object Object]' in [students | sortByName:queryElem.value

Laut der Antwort von @ drawmoore "tritt dieser Fehler nur im Entwicklungsmodus auf (der ab Beta-0 standardmäßig aktiviert ist). Wenn Sie enableProdMode()beim Booten der App aufrufen , wird der Fehler nicht ausgelöst." Die Dokumente für denApplicationRef.tick() Status:

Im Entwicklungsmodus führt tick () auch einen zweiten Änderungserkennungszyklus durch, um sicherzustellen, dass keine weiteren Änderungen erkannt werden. Wenn während dieses zweiten Zyklus zusätzliche Änderungen erfasst werden, haben Bindungen in der App Nebenwirkungen, die nicht in einem einzigen Änderungserkennungsdurchlauf behoben werden können. In diesem Fall gibt Angular einen Fehler aus, da eine Angular-Anwendung nur einen Änderungserkennungsdurchlauf haben kann, während dessen die gesamte Änderungserkennung abgeschlossen sein muss.

In unserem Szenario glaube ich, dass der Fehler falsch / irreführend ist. Wir haben eine Stateful-Pipe, und die Ausgabe kann sich bei jedem Aufruf ändern - sie kann Nebenwirkungen haben, und das ist in Ordnung. NgFor wird nach dem Rohr ausgewertet, daher sollte es gut funktionieren.

Wir können uns jedoch nicht wirklich entwickeln, wenn dieser Fehler ausgelöst wird. Eine Problemumgehung besteht darin, der Pipe-Implementierung eine Array-Eigenschaft (dh einen Status) hinzuzufügen und dieses Array immer zurückzugeben. Siehe die Antwort von @ pixelbits für diese Lösung.

Wir können jedoch effizienter sein, und wie wir sehen werden, benötigen wir die Array-Eigenschaft in der Pipe-Implementierung nicht und wir benötigen keine Problemumgehung für die Doppeländerungserkennung.

Erkennung von Komponentenänderungen

Standardmäßig durchläuft die Erkennung von Winkeländerungen bei jedem Browserereignis jede Komponente, um festzustellen, ob sie sich geändert hat. Eingaben und Vorlagen (und möglicherweise andere Dinge?) Werden überprüft.

Wenn wir wissen, dass eine Komponente nur von ihren Eingabeeigenschaften (und Vorlagenereignissen) abhängt und dass die Eingabeeigenschaften unveränderlich sind, können wir die wesentlich effizientere onPushStrategie zur Änderungserkennung verwenden. Bei dieser Strategie wird anstelle jedes Browserereignisses eine Komponente nur dann überprüft, wenn sich die Eingaben ändern und wenn Vorlagenereignisse ausgelöst werden. Und anscheinend bekommen wir diesen Expression ... has changed after it was checkedFehler mit dieser Einstellung nicht. Dies liegt daran, dass eine onPushKomponente erst dann erneut überprüft wird, wenn sie erneut "markiert" ( ChangeDetectorRef.markForCheck()) ist. Template-Bindungen und Stateful-Pipe-Ausgaben werden also nur einmal ausgeführt / ausgewertet. Zustandslose / reine Pipes werden immer noch nicht ausgeführt, es sei denn, ihre Eingaben ändern sich. Also brauchen wir hier noch eine Stateful Pipe.

Dies ist die von @EricMartinez vorgeschlagene Lösung: Stateful Pipe mit onPushÄnderungserkennung. Siehe @ caffinatedmonkeys Antwort für diese Lösung.

Beachten Sie, dass bei dieser Lösung die transform()Methode nicht jedes Mal dasselbe Array zurückgeben muss. Ich finde das allerdings etwas seltsam: eine zustandsbehaftete Pipe ohne Zustand. Denken Sie noch etwas darüber nach ... die Stateful Pipe sollte wahrscheinlich immer das gleiche Array zurückgeben. Andernfalls könnte es nur mit onPushKomponenten im Dev-Modus verwendet werden.


Nach all dem mag ich eine Kombination aus den Antworten von @ Eric und @ pixelbits: Stateful Pipe, die dieselbe Array-Referenz zurückgibt, mit onPushÄnderungserkennung, wenn die Komponente dies zulässt. Da die Stateful-Pipe dieselbe Array-Referenz zurückgibt, kann die Pipe weiterhin mit Komponenten verwendet werden, die nicht mit konfiguriert sind onPush.

Plunker

Dies wird wahrscheinlich zu einer Angular 2-Redewendung: Wenn ein Array eine Pipe speist und sich das Array möglicherweise ändert (die Elemente im Array, nicht die Array-Referenz), müssen wir eine Stateful-Pipe verwenden.

Mark Rajcok
quelle
Vielen Dank für Ihre ausführliche Erklärung des Problems. Es ist jedoch seltsam, dass die Änderungserkennung von Arrays als Identität anstelle eines Gleichheitsvergleichs implementiert wird. Wäre es möglich, dies mit Observable zu lösen?
Martin Nowak
@MartinNowak, wenn Sie fragen, ob das Array ein Observable und die Pipe zustandslos ist ... Ich weiß nicht, das habe ich nicht versucht.
Mark Rajcok
Eine andere Lösung wäre eine reine Pipe, bei der das Ausgabearray Änderungen im Eingabearray mit Ereignis-Listenern verfolgt.
Tuupertunut
Ich habe gerade Ihren Plunker gegabelt und es funktioniert für mich ohne onPushÄnderungserkennung plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan
27

Wie Eric Martinez in den Kommentaren betonte, wird Ihr Problem durch Hinzufügen pure: falsezu Ihrem PipeDekorateur und changeDetection: ChangeDetectionStrategy.OnPushIhrem ComponentDekorateur behoben. Hier ist ein funktionierender Plunkr. Wechseln zuChangeDetectionStrategy.Always , funktioniert auch. Hier ist der Grund.

Entsprechend der Winkelführung2 an Rohren :

Pipes sind standardmäßig zustandslos. Wir müssen eine Pipe als zustandsbehaftet deklarieren, indem wir die pureEigenschaft des @PipeDekorateurs auf setzenfalse . Diese Einstellung weist das Änderungserkennungssystem von Angular an, die Ausgabe dieses Rohrs in jedem Zyklus zu überprüfen, ob sich die Eingabe geändert hat oder nicht.

Wie für die in ChangeDetectionStrategyder Standardeinstellung werden alle Bindungen jeden einzelnen Zyklus überprüft. Wenn eine pure: falsePipe hinzugefügt wird, ändert sich meines Erachtens die Änderungserkennungsmethode aus Leistungsgründen von von CheckAlwaysnach CheckOnce. Mit OnPushwerden Bindungen für die Komponente nur überprüft, wenn sich eine Eingabeeigenschaft ändert oder wenn ein Ereignis ausgelöst wird. Weitere Informationen zu Änderungsdetektoren, ein wichtiger Bestandteil von angular2, finden Sie unter den folgenden Links:

0xcaff
quelle
"Wenn eine pure: falsePipe hinzugefügt wird, ändert sich meiner Meinung nach die Änderungserkennungsmethode von CheckAlways zu CheckOnce" - das stimmt nicht mit dem überein, was Sie aus den Dokumenten zitiert haben. Mein Verständnis ist wie folgt: Die Eingaben einer zustandslosen Pipe werden standardmäßig in jedem Zyklus überprüft. Bei einer Änderung wird die Pipe- transform()Methode aufgerufen, um die Ausgabe (neu) zu generieren. Die transform()Methode einer Stateful Pipe wird in jedem Zyklus aufgerufen und ihre Ausgabe auf Änderungen überprüft. Siehe auch stackoverflow.com/a/34477111/215945 .
Mark Rajcok
@ MarkRajcok, Ups, Sie haben Recht, aber wenn dies der Fall ist, warum funktioniert die Änderung der Änderungserkennungsstrategie?
0xcaff
1
Gute Frage. Es scheint, dass, wenn die Änderungserkennungsstrategie geändert wird OnPush(dh die Komponente wird als "unveränderlich" markiert), die Stateful-Pipe-Ausgabe nicht "stabilisiert" werden muss - dh die transform()Methode wird anscheinend nur einmal ausgeführt (wahrscheinlich alle) Die Änderungserkennung für die Komponente wird nur einmal ausgeführt. Vergleichen Sie dies mit der Antwort von @ pixelbits, bei der die transform()Methode mehrmals aufgerufen wird und sich stabilisieren muss (gemäß der anderen Antwort von pixelbits ), weshalb dieselbe Array-Referenz verwendet werden muss.
Mark Rajcok
@MarkRajcok Wenn das, was Sie sagen, in Bezug auf die Effizienz richtig ist, ist dies in diesem speziellen Anwendungsfall wahrscheinlich der bessere Weg, da transformes nur aufgerufen wird, wenn neue Daten anstatt mehrmals gepusht werden, bis sich die Ausgabe stabilisiert, oder?
0xcaff
1
Wahrscheinlich sehen Sie meine langatmige Antwort. Ich wünschte, es gäbe mehr Dokumentation über die Erkennung von Änderungen in Angular 2.
Mark Rajcok
22

Demo Plunkr

Sie müssen die ChangeDetectionStrategy nicht ändern. Die Implementierung eines Stateful Pipe reicht aus, um alles zum Laufen zu bringen.

Dies ist eine Stateful Pipe (es wurden keine weiteren Änderungen vorgenommen):

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  tmp = [];
  transform (value, [queryString]) {
    this.tmp.length = 0;
    // console.log(value, queryString);
    var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
    for (var i =0; i < arr.length; ++i) {
        this.tmp.push(arr[i]);
     }

    return this.tmp;
  }
}
Pixelbits
quelle
Ich habe diesen Fehler erhalten, ohne changeDectection in onPush zu ändern: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview lightboxes( Plnkr)Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]
Chu Son
1
Können Sie ein Beispiel dafür geben, warum Sie Werte in einer lokalen Variablen in SortByNamePipe speichern und dann in der Transformationsfunktion @pixelbits zurückgeben müssen? Ich bemerkte auch, dass der Winkelzyklus durch die Transformationsfunktion zweimal mit changeDetetion.OnPush und viermal ohne diese Funktion
Chu Son
@ChuSon, Sie sehen sich wahrscheinlich Protokolle einer früheren Version an. Anstatt ein Array von Werten zurückzugeben, geben wir einen Verweis auf ein Array von Werten zurück, die meiner Meinung nach geändert werden können. Pixelbits, Ihre Antwort macht mehr Sinn.
0xcaff
Zum Nutzen anderer Leser wurde hinsichtlich des oben in den Kommentaren erwähnten Fehlers ChuSon und der Notwendigkeit, eine lokale Variable zu verwenden, eine weitere Frage erstellt und mit Pixelbits beantwortet.
Mark Rajcok
19

Aus der eckigen Dokumentation

Reine und unreine Rohre

Es gibt zwei Kategorien von Rohren: rein und unrein. Rohre sind standardmäßig rein. Jede Pfeife, die Sie bisher gesehen haben, war rein. Sie machen ein Rohr unrein, indem Sie sein reines Flag auf false setzen. Sie könnten die FlyingHeroesPipe so unrein machen:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

Bevor Sie dies tun, sollten Sie den Unterschied zwischen rein und unrein verstehen, beginnend mit einem reinen Rohr.

Reine Rohre Angular führt ein reines Rohr nur aus, wenn eine reine Änderung des Eingabewerts festgestellt wird. Eine reine Änderung ist entweder eine Änderung eines primitiven Eingabewerts (String, Number, Boolean, Symbol) oder einer geänderten Objektreferenz (Datum, Array, Funktion, Objekt).

Angular ignoriert Änderungen in (zusammengesetzten) Objekten. Es wird keine reine Pipe aufgerufen, wenn Sie einen Eingabemonat ändern, einem Eingabearray hinzufügen oder eine Eingabeobjekteigenschaft aktualisieren.

Dies mag restriktiv erscheinen, ist aber auch schnell. Eine Objektreferenzprüfung ist schnell - viel schneller als eine gründliche Prüfung auf Unterschiede -, sodass Angular schnell feststellen kann, ob sowohl die Pipe-Ausführung als auch eine Ansichtsaktualisierung übersprungen werden können.

Aus diesem Grund ist eine reine Pipe vorzuziehen, wenn Sie mit der Änderungserkennungsstrategie leben können. Wenn Sie nicht können, können Sie das unreine Rohr verwenden.

Sumit Jaiswal
quelle
Die Antwort ist richtig, aber ein bisschen mehr Aufwand beim Posten wäre schön
Stefan
2

Anstatt rein zu machen: falsch. Sie können den Wert in der Komponente tief kopieren und durch this.students = Object.assign ([], NEW_ARRAY) ersetzen. Dabei ist NEW_ARRAY das geänderte Array.

Es funktioniert für Winkel 6 und sollte auch für andere Winkelversionen funktionieren.

ideeps
quelle
0

Eine Problemumgehung: Importieren Sie Pipe manuell in den Konstruktor und rufen Sie die Transformationsmethode mit dieser Pipe auf

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

Eigentlich brauchst du nicht mal eine Pfeife

Richard Luo
quelle
0

Fügen Sie dem zusätzlichen Parameter Pipe hinzu und ändern Sie ihn direkt nach dem Ändern des Arrays. Selbst bei einer reinen Pipe wird die Liste aktualisiert

Artikel von Gegenständen lassen | Rohr: param

Nick Bistrov
quelle
0

In diesem Anwendungsfall habe ich meine Pipe in ts-Datei zum Filtern von Daten verwendet. Es ist viel besser für die Leistung als die Verwendung von reinen Rohren. Verwenden Sie in ts wie folgt:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}
Andris
quelle
0

unreine Rohre herzustellen, ist bei der Leistung kostspielig. Erstellen Sie also keine unreinen Rohre, sondern ändern Sie die Referenz der Datenvariablen, indem Sie bei Datenänderungen eine Kopie der Daten erstellen und die Referenz der Kopie in der ursprünglichen Datenvariablen neu zuweisen.

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
amit bhandari
quelle