NgFor nie aktualizuje danych za pomocą Pipe w Angular2

114

W tym scenariuszu wyświetlam listę uczniów (tablicę) do widoku z ngFor:

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

To wspaniałe, że aktualizuje się za każdym razem, gdy dodam innego ucznia do listy.

Jednak, gdy daję się pipedo filtero imieniu studenta,

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

Nie aktualizuje listy, dopóki nie wpiszę czegoś w filtrującym polu nazwiska ucznia.

Oto link do 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 Son
źródło
14
Dodaj pure:falsew swojej rurze i changeDetection: ChangeDetectionStrategy.OnPushkomponencie.
Eric Martinez
2
Dzięki @EricMartinez. To działa. Ale czy możesz trochę wyjaśnić?
Chu Son
2
Sugerowałbym również, aby NIE używać .test()funkcji filtrującej. Dzieje się tak, ponieważ jeśli użytkownik wprowadzi ciąg zawierający znaki specjalne, takie jak: *lub +itp., Twój kod się zepsuje. Myślę, że powinieneś użyć.includes() lub wyjść z ciągu zapytania z funkcją niestandardową.
Eggy
6
Dodanie pure:falsei ustawienie stanu potoku rozwiąże problem. Modyfikowanie ChangeDetectionStrategy nie jest konieczne.
pixelbits
2
Dla każdego, kto to czyta, dokumentacja dotycząca rur kątowych stała się znacznie lepsza i zawiera wiele omówionych tutaj kwestii. Sprawdź to.
0xcaff

Odpowiedzi:

154

Aby w pełni zrozumieć problem i możliwe rozwiązania, musimy omówić wykrywanie zmian kątowych - dla rur i komponentów.

Wykrywanie zmian rur

Rury bezpaństwowe / czyste

Domyślnie potoki są bezstanowe / czyste. Potoki bezstanowe / czyste po prostu przekształcają dane wejściowe w dane wyjściowe. Nic nie pamiętają, więc nie mają żadnych właściwości - tylko transform()metoda. Angular może zatem zoptymalizować traktowanie rur bezstanowych / czystych: jeśli ich dane wejściowe się nie zmieniają, rury nie muszą być wykonywane podczas cyklu wykrywania zmian. W przypadku rur takich jak {{power | exponentialStrength: factor}}, powerifactor są wejściami.

W przypadku tego pytania "#student of students | sortByName:queryElem.value", studentsiqueryElem.value to wejścia, a przewód sortByNamejest bezstanowy / czystego. studentsjest tablicą (odniesieniem).

  • Po dodaniu ucznia tablica odwołanie nie zmienia się -students nie zmienia się - dlatego potok bezstanowy / czysty nie jest wykonywany.
  • Kiedy coś jest wpisane na wejściu filtru, queryElem.valuezmienia się, dlatego wykonywany jest potok bezstanowy / czysty.

Jednym ze sposobów rozwiązania problemu z tablicą jest zmiana odwołania do tablicy za każdym razem, gdy dodawany jest uczeń, tj. Tworzenie nowej tablicy za każdym razem, gdy dodawany jest uczeń. Moglibyśmy to zrobić za pomocą concat():

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

Chociaż to działa, nasz addNewStudent() metoda nie powinna być implementowana w określony sposób tylko dlatego, że używamy potoku. Chcemy push()dodać do naszej tablicy.

Stateful Pipes

Rury stanowe mają stan - zwykle mają właściwości, a nie tylko transform()metodę. Mogą wymagać oceny, nawet jeśli ich dane wejściowe nie uległy zmianie. Kiedy określimy, że potok jest stanowy / nieczysty -pure: false - wtedy za każdym razem, gdy system wykrywania zmian Angulara sprawdza komponent pod kątem zmian i ten komponent używa potoku stanowego, sprawdza wyjście potoku, czy jego wejście uległo zmianie, czy nie.

Brzmi to tak, jak chcemy, nawet jeśli jest mniej wydajne, ponieważ chcemy, aby potok był wykonywany, nawet jeśli studentsodniesienie nie uległo zmianie. Jeśli po prostu sprawimy, że potok będzie stanowy, otrzymamy błąd:

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

Zgodnie z odpowiedzią @ drewmoore , „ten błąd występuje tylko w trybie programisty (który jest domyślnie włączony od wersji beta-0). Jeśli zadzwonisz enableProdMode()podczas ładowania aplikacji, błąd nie zostanie zgłoszony”. Dokumenty dotycząceApplicationRef.tick() stanu:

W trybie programowania tick () wykonuje również drugi cykl wykrywania zmian, aby zapewnić, że żadne dalsze zmiany nie zostaną wykryte. Jeśli w tym drugim cyklu zostaną wykryte dodatkowe zmiany, powiązania w aplikacji mają skutki uboczne, których nie można rozwiązać w pojedynczym przebiegu wykrywania zmian. W takim przypadku Angular zgłasza błąd, ponieważ aplikacja Angular może mieć tylko jeden przebieg wykrywania zmiany, podczas którego musi zostać zakończone wszystkie wykrywanie zmian.

W naszym scenariuszu uważam, że błąd jest fałszywy / wprowadzający w błąd. Mamy potok stanowy i wyjście może się zmieniać za każdym razem, gdy jest wywoływane - może mieć efekty uboczne i to jest w porządku. NgFor jest oceniane po potoku, więc powinno działać poprawnie.

Jednak tak naprawdę nie możemy programować po wyrzuceniu tego błędu, więc jednym obejściem jest dodanie właściwości tablicy (tj. Stan) do implementacji potoku i zawsze zwracanie tej tablicy. Zobacz odpowiedź @ pixelbits na to rozwiązanie.

Możemy jednak być bardziej wydajni i, jak zobaczymy, nie będziemy potrzebować właściwości tablicy w implementacji potoku i nie będziemy potrzebować obejścia dla wykrywania podwójnej zmiany.

Wykrywanie zmian komponentów

Domyślnie przy każdym zdarzeniu przeglądarki wykrywanie zmian kątowych przechodzi przez każdy komponent, aby sprawdzić, czy się zmienił - sprawdzane są dane wejściowe i szablony (a może inne rzeczy?).

Jeśli wiemy, że składnik zależy tylko od jego właściwości wejściowych (i zdarzeń szablonu), a właściwości wejściowe są niezmienne, możemy zastosować znacznie wydajniejszą onPushstrategię wykrywania zmian. Dzięki tej strategii, zamiast sprawdzania każdego zdarzenia przeglądarki, komponent jest sprawdzany tylko wtedy, gdy zmieniają się dane wejściowe i gdy wyzwalają się zdarzenia szablonu. I najwyraźniej nie otrzymujemy tego Expression ... has changed after it was checkedbłędu przy tym ustawieniu. Dzieje się tak, ponieważ onPushskładnik nie jest ponownie sprawdzany, dopóki nie zostanie ponownie „zaznaczony” ( ChangeDetectorRef.markForCheck()). Dlatego powiązania szablonów i stanowe dane wyjściowe potoku są wykonywane / oceniane tylko raz. Potoki bezstanowe / czyste nadal nie są wykonywane, chyba że zmienią się ich dane wejściowe. Więc nadal potrzebujemy tu stanowej potoku.

Oto rozwiązanie zasugerowane przez @EricMartinez: potok stanowy z onPush wykrywaniem zmian. Zobacz odpowiedź @ caffinatedmonkey dla tego rozwiązania.

Zauważ, że w tym rozwiązaniu transform()metoda nie musi za każdym razem zwracać tej samej tablicy. Uważam to jednak za trochę dziwne: potok stanowy bez stanu. Myśląc o tym więcej ... potok stanowy prawdopodobnie powinien zawsze zwracać tę samą tablicę. W przeciwnym razie mógłby być używany tylko z onPushkomponentami w trybie deweloperskim.


Po tym wszystkim, myślę, że podoba mi się kombinacja odpowiedzi @ Erica i @ pixelbits: potok stanowy, który zwraca to samo odniesienie do tablicy, z onPushwykrywaniem zmian, jeśli komponent na to pozwala. Ponieważ potok stanowy zwraca to samo odwołanie do tablicy, potok może być nadal używany z komponentami, które nie są skonfigurowane z onPush.

Plunker

Prawdopodobnie stanie się to idiomem Angulara 2: jeśli tablica zasila potok, a tablica może się zmienić (elementy w tablicy, a nie odwołanie do tablicy), musimy użyć potoku stanowego.

Mark Rajcok
źródło
Dziękuję za szczegółowe wyjaśnienie problemu. Dziwne jest jednak to, że wykrywanie zmian tablic jest implementowane jako tożsamość zamiast porównania równości. Czy byłoby możliwe rozwiązanie tego problemu za pomocą Observable?
Martin Nowak
@MartinNowak, jeśli pytasz, czy tablica była Observable, a potok był bezstanowy ... Nie wiem, nie próbowałem tego.
Mark Rajcok
Innym rozwiązaniem byłby czysty potok, w którym tablica wyjściowa śledzi zmiany w tablicy wejściowej za pomocą detektorów zdarzeń.
Tuupertunut
Właśnie rozwidliłem twojego Plunkera i działa dla mnie bez onPushwykrywania zmian plnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan
28

Eric Martinez zauważył w komentarzach, dodając pure: falsedo Pipedekoratora i changeDetection: ChangeDetectionStrategy.OnPushdo Componentdekoratora będzie rozwiązać problem. Oto pracujący plunkr. Zmiana na ChangeDetectionStrategy.Alwaysrównież działa. Dlatego.

Zgodnie z przewodnikiem angular2 na rurach :

Potoki są domyślnie bezstanowe. Musimy zadeklarować, że potok jest stanowy, ustawiając purewłaściwość @Pipedekoratora na false. To ustawienie mówi systemowi wykrywania zmian Angulara, aby sprawdzał wyjście tej rury w każdym cyklu, czy jego wejście uległo zmianie, czy nie.

Jeśli chodzi o ChangeDetectionStrategy , domyślnie wszystkie powiązania są sprawdzane w każdym cyklu. Gdy pure: falserura jest dodany, to uważa, metody wykrywania zmiany zmienia się od CheckAlwayssię CheckOnceze względu na wydajność. W przypadku OnPushpowiązania dla składnika są sprawdzane tylko w przypadku zmiany właściwości wejściowej lub wyzwolenia zdarzenia. Aby uzyskać więcej informacji o wykrywaczach zmian, ważnej części angular2, zapoznaj się z poniższymi linkami:

0xcaff
źródło
„Po dodaniu pure: falsepotoku myślę, że metoda wykrywania zmian zmienia się z CheckAlways na CheckOnce” - to nie zgadza się z tym, co cytowałeś z dokumentacji. Moje rozumienie jest następujące: dane wejściowe potoku bezstanowego są domyślnie sprawdzane w każdym cyklu. W przypadku zmiany transform()wywoływana jest metoda potoku w celu (ponownego) wygenerowania wyniku. transform()Metoda stanowego potoku jest wywoływana w każdym cyklu, a jej wyjście jest sprawdzane pod kątem zmian. Zobacz też stackoverflow.com/a/34477111/215945 .
Mark Rajcok,
@MarkRajcok, Ups, masz rację, ale jeśli tak jest, dlaczego działa zmiana strategii wykrywania zmian?
0xcaff
1
Świetne pytanie. Wydawałoby się, że kiedy strategia wykrywania zmian zostanie zmieniona na OnPush(tj. Komponent jest oznaczony jako „niezmienny”), stanowy potok wyjściowy nie musi się „stabilizować” - tj. Wydaje się, że transform()metoda jest uruchamiana tylko raz (prawdopodobnie wszystkie wykrywanie zmian dla komponentu jest uruchamiane tylko raz). Porównaj to z odpowiedzią @ pixelbits, w której transform()metoda jest wywoływana wielokrotnie i musi się ustabilizować (zgodnie z inną odpowiedzią pixelbits ), stąd potrzeba użycia tego samego odniesienia do tablicy.
Mark Rajcok
@MarkRajcok Jeśli to, co mówisz, jest poprawne pod względem wydajności, jest to prawdopodobnie lepszy sposób, w tym konkretnym przypadku użycia, ponieważ transformjest wywoływany tylko wtedy, gdy nowe dane są przesyłane, a nie wiele razy, aż dane wyjściowe się ustabilizują, prawda?
0xcaff
1
Prawdopodobnie zobacz moją rozwlekłą odpowiedź. Chciałbym, żeby było więcej dokumentacji na temat wykrywania zmian w Angular 2.
Mark Rajcok,
22

Demo Plunkr

Nie musisz zmieniać ChangeDetectionStrategy. Wdrożenie stanowego potoku wystarczy, aby wszystko działało.

To jest potok stanowy (żadne inne zmiany nie zostały wprowadzone):

@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;
  }
}
bity pikseli
źródło
Otrzymałem ten błąd bez zmiany changeDectection na onPush: [ plnkr.co/edit/j1VUdgJxYr6yx8WgzCId?p=preview](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
Czy możesz na przykład, dlaczego musisz przechowywać wartości w zmiennej lokalnej w SortByNamePipe, a następnie zwracać je w funkcji transformacji @pixelbits? Zauważyłem również, że Angular przechodzi dwukrotnie przez funkcję transformacji z changeDetetion.OnPush i 4 razy bez niej
Chu Son
@ChuSon, prawdopodobnie przeglądasz dzienniki z poprzedniej wersji. Zamiast zwracać tablicę wartości, zwracamy odwołanie do tablicy wartości, które, jak sądzę, można wykryć. Pixelbits, twoja odpowiedź ma więcej sensu.
0xcaff
Z korzyścią dla innych czytelników, w związku z błędem ChuSon, o którym mowa powyżej w komentarzach, i potrzebą użycia zmiennej lokalnej, stworzono kolejne pytanie , na które udzielono odpowiedzi pikselami.
Mark Rajcok,
19

Z dokumentacji kątowej

Czyste i nieczyste rury

Istnieją dwie kategorie rur: czyste i nieczyste. Rury są domyślnie czyste. Każda fajka, którą do tej pory widziałeś, była czysta. Czyścisz rurę, ustawiając jej czystą flagę na fałsz. Możesz uczynić FlyingHeroesPipe nieczystym w następujący sposób:

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

Zanim to zrobisz, zrozum różnicę między czystym a nieczystym, zaczynając od czystej fajki.

Czyste rury Angular wykonuje czystą potokę tylko wtedy, gdy wykryje czystą zmianę wartości wejściowej. Czysta zmiana to albo zmiana pierwotnej wartości wejściowej (ciąg, liczba, wartość logiczna, symbol), albo zmieniona referencja do obiektu (data, tablica, funkcja, obiekt).

Angular ignoruje zmiany w obiektach (złożonych). Nie wywoła czystego potoku, jeśli zmienisz miesiąc wejściowy, dodasz do tablicy wejściowej lub zaktualizujesz właściwość obiektu wejściowego.

Może się to wydawać restrykcyjne, ale jest też szybkie. Sprawdzenie odniesienia do obiektu jest szybkie - znacznie szybsze niż dokładne sprawdzenie różnic - dzięki czemu Angular może szybko określić, czy może pominąć zarówno wykonanie potoku, jak i aktualizację widoku.

Z tego powodu czysta potok jest preferowana, gdy możesz żyć ze strategią wykrywania zmian. Jeśli nie możesz, możesz użyć nieczystej rury.

Sumit Jaiswal
źródło
Odpowiedź jest poprawna, ale trochę więcej wysiłku przy zamieszczaniu byłoby fajnie
Stefan
2

Zamiast robić czyste: fałszywe. Możesz głęboko skopiować i zamienić wartość w komponencie na this.students = Object. assign ([], NEW_ARRAY); gdzie NEW_ARRAY to zmodyfikowana tablica.

Działa dla kątowej 6 i powinien działać również z innymi wersjami kątowymi.

ideeps
źródło
0

Obejście problemu: ręcznie zaimportuj potok do konstruktora i wywołaj metodę transformacji przy użyciu tego potoku

constructor(
private searchFilter : TableFilterPipe) { }

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

Właściwie nie potrzebujesz nawet fajki

Richard Luo
źródło
0

Dodaj do potoku dodatkowy parametr i zmień go zaraz po zmianie tablicy, a nawet przy czystym potoku lista zostanie odświeżona

niech pozycja przedmiotów | pipe: param

Nick Bistrov
źródło
0

W tym przypadku użyłem mojego potoku w pliku ts do filtrowania danych. Jest to znacznie lepsze dla wydajności niż używanie czystych rur. Użyj w ts w ten sposób:

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}
Andris
źródło
0

tworzenie zanieczyszczonych rur jest kosztowne w wydajności. więc nie twórz nieczystych potoków, raczej zmień odniesienie do zmiennej danych, tworząc kopię danych przy zmianie danych i ponownie przypisuj odniesienie do kopii w oryginalnej zmiennej danych.

            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
źródło