Wyrażenie ___ zmieniło się po sprawdzeniu

286

Dlaczego jest składnikiem tego prostego wyłożenia

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message:string = 'loading :(';

  ngAfterViewInit() {
    this.updateMessage();
  }

  updateMessage(){
    this.message = 'all done loading :)'
  }
}

rzucanie:

WYJĄTEK: Wyrażenie „Jestem {{message}} w aplikacji @ 0: 5” zmieniło się po sprawdzeniu. Poprzednia wartość: „Ładuję :(”. Aktualna wartość: „ładowanie zostało zakończone :)” w [I'm {{message}} w aplikacji @ 0: 5]

kiedy wszystko, co robię, to aktualizowanie prostego wiązania po zainicjowaniu mojego widoku?

narysował Moore
źródło
7
Artykuł Wszystko, co musisz wiedzieć o ExpressionChangedAfterItHasBeenCheckedErrorbłędzie, wyjaśnia szczegółowo to zachowanie.
Max Koretskyi
Zastanów się nad modyfikacją swojego konta ChangeDetectionStrategypodczas korzystania z detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas,
Pomyśl tylko o tym, że istnieje kontrola wejściowa i wypełniasz ją danymi w metodzie, a w tej samej metodzie przypisujesz jej pewną wartość. Kompilator na pewno zostanie pomylony z nową / poprzednią wartością. Zatem wiązanie i wypełnianie powinno odbywać się różnymi metodami.
MAC

Odpowiedzi:

293

Jak stwierdził Drawmoore, właściwym rozwiązaniem w tym przypadku jest ręczne uruchomienie wykrywania zmian dla bieżącego komponentu. Odbywa się to za pomocą detectChanges()metody ChangeDetectorRefobiektu (importowanego z angular2/core) lub jej markForCheck()metody, która powoduje również aktualizację wszystkich komponentów nadrzędnych. Odpowiedni przykład :

import { Component, ChangeDetectorRef, AfterViewInit } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App implements AfterViewInit {
  message: string = 'loading :(';

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.message = 'all done loading :)'
    this.cdr.detectChanges();
  }

}

Oto również narzędzia Plunkers demonstrujące na wszelki wypadek podejścia ngOnInit , setTimeout i enableProdMode .

Kiara Grouwstra
źródło
7
W moim przypadku otwierałem modal. Po otwarciu modalu wyświetlał komunikat „Wyrażenie ___ zmieniło się po sprawdzeniu”, więc moje rozwiązanie zostało dodane this.cdr.detectChanges (); po otwarciu mojego modalu. Dzięki!
Jorge Casariego,
Gdzie jest twoje zgłoszenie własności cdr? Spodziewałbym się zobaczyć taką deklarację cdr : anylub coś takiego w messagedeklaracji. Martwisz się, że coś przeoczyłem?
CodeCabbie,
1
@CodeCabbie jest w argumentach konstruktora.
slasky
1
Ta rozdzielczość rozwiązuje mój problem! Wielkie dzięki! Bardzo jasny i prosty sposób.
lwozniak
3
Pomogło mi to - z kilkoma modyfikacjami. Musiałem stylizować elementy li, które zostały wygenerowane przez pętlę ngFor. Musiałem zmienić kolor zakresów w elementach listy na podstawie tekstu wewnętrznego po kliknięciu przycisku „sortuj”, który zaktualizował wartość bool używaną jako parametr potoku sortowania (posortowany wynik był kopią danych, więc style nie były wyświetlane zaktualizowane tylko o ngStyle). - Zamiast „AfterViewInit” użyłem „AfterViewChecked” - upewniłem się również, że zaimportowałem i zaimplementowałem AfterViewChecked. Uwaga: ustawienie fajki na „czysto: fałsz” nie działało, musiałem dodać ten dodatkowy krok (:
Chloe Corrigan
170

Po pierwsze, zwróć uwagę, że ten wyjątek zostanie zgłoszony tylko wtedy, gdy uruchomisz aplikację w trybie deweloperskim (co ma miejsce domyślnie w wersji beta-0): Jeśli zadzwonisz enableProdMode()podczas ładowania aplikacji, nie zostanie ona zgłoszona ( zobacz zaktualizowane plunk ).

Po drugie, nie rób tego, ponieważ ten wyjątek jest zgłaszany z ważnego powodu: krótko mówiąc, w trybie deweloperskim po każdej rundzie wykrywania zmian następuje natychmiast druga runda, która weryfikuje, czy żadne powiązania nie uległy zmianie od końca pierwszej, ponieważ oznaczałoby to, że zmiany są powodowane przez samo wykrywanie zmian.

W twojej grze, wiązanie {{message}}jest zmieniane przez twoje wezwanie do setMessage(), które dzieje się w ngAfterViewInithaku, które występuje jako część początkowej tury wykrywania zmian. To samo w sobie nie jest jednak problematyczne - problem polega na tym, że setMessage()zmienia to wiązanie, ale nie wyzwala nowej rundy wykrywania zmian, co oznacza, że ​​ta zmiana nie zostanie wykryta, dopóki jakaś inna runda wykrywania zmian nie zostanie uruchomiona w innym miejscu.

Na wynos: Wszystko, co zmienia wiązanie, musi wywołać rundę wykrywania zmian, kiedy to nastąpi .

Zaktualizuj w odpowiedzi na wszystkie prośby o przykład, jak to zrobić : rozwiązanie @ Tycho działa, podobnie jak trzy metody w odpowiedzi @ MarkRajcok wskazał. Ale szczerze mówiąc, wszyscy czują się dla mnie brzydcy i źli, jak hacki, do których przyzwyczailiśmy się w ng1.

Aby mieć pewność, że są sporadyczne sytuacje, w których te hacki są odpowiednie, ale jeśli używasz ich na coś więcej niż tylko bardzo sporadycznie, to znak, że walczysz ramy zamiast całkowicie obejmując jego reaktywny charakter.

IMHO, bardziej idiomatyczny, „Angular2 sposób” podejścia do tego jest coś w stylu: ( plunk )

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message | async}} </div>`
})
export class App {
  message:Subject<string> = new BehaviorSubject('loading :(');

  ngAfterViewInit() {
    this.message.next('all done loading :)')
  }
}
narysował Moore
źródło
13
Dlaczego setMessage () nie wyzwala nowej rundy wykrywania zmian? Myślałem, że Angular 2 automatycznie wyzwalał wykrywanie zmian, kiedy zmieniasz wartość czegoś w interfejsie użytkownika.
Danny Libin
4
@drewmoore „Wszystko, co zmienia powiązanie, musi wywołać rundę wykrywania zmian, kiedy to nastąpi”. W jaki sposób? Czy to dobra praktyka? Czy nie powinno się wszystko skończyć za jednym razem?
Daniel Birowsky Popeski,
2
@ Tycho, rzeczywiście. Ponieważ napisałem ten komentarz, odpowiedziałem na inne pytanie, w którym opisuję 3 sposoby uruchamiania wykrywania zmian , które obejmują detectChanges().
Mark Rajcok
4
tylko zauważyć, że w bieżącym pytaniu nazwa wywoływanej metody jest nazwana updateMessage, a niesetMessage
superjos
3
@Daynil, miałem to samo uczucie, dopóki nie przeczytałem bloga podanego w komentarzu pod pytaniem: blog.angularindepth.com/... To wyjaśnia, dlaczego należy to zrobić ręcznie. W tym przypadku wykrywanie zmian kąta ma cykl życia. W przypadku, gdy coś zmienia wartość pomiędzy tymi cyklami życia, należy uruchomić wymuszone wykrywanie zmian (lub ustalenie czasu - które zostanie wykonane w pętli następnego zdarzenia, ponownie wyzwalając wykrywanie zmiany).
Mahesh
50

ngAfterViewChecked() pracował dla mnie:

import { Component, ChangeDetectorRef } from '@angular/core'; //import ChangeDetectorRef

constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
   //your code to update the model
   this.cdr.detectChanges();
}
Jaswanth Kumar
źródło
Jak już wspomniano, cykl wykrywania zmian kąta, który jest dwufazowy, musi wykrywać zmiany po zmodyfikowaniu widoku dziecka, i uważam, że najlepszym sposobem na to jest użycie haka cyklu życia dostarczonego przez sam kątownik i proszenie go, aby ręcznie wykryj zmianę i powiąż ją. Moim osobistym zdaniem jest to odpowiednia odpowiedź.
Joey587,
1
Działa to dla mnie, aby wczytać razem dynamiczny komponent hierarchii.
Mehdi Daustany
47

Naprawiłem to, dodając ChangeDetectionStrategy z kątowego rdzenia.

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})
Biranchi
źródło
To zadziałało dla mnie. Zastanawiam się, jaka jest różnica między tym a użyciem ChangeDetectorRef
Ari
1
Hmm ... Wtedy tryb detektora zmian zostanie początkowo ustawiony na CheckOnce( dokumentacja )
Alex Klaus
Tak, błąd / ostrzeżenie zniknęły. Ale zajmuje to dużo czasu ładowania niż wcześniej, jak różnica 5-7 sekund, co jest ogromne.
Kapil Raghuwanshi
1
@KapilRaghuwanshi Uruchom this.cdr.detectChanges();po tym, co próbujesz załadować. Ponieważ może tak być, ponieważ wykrywanie zmian nie uruchamia się
Harshal Carpenter
36

Nie możesz użyć, ngOnInitponieważ właśnie zmieniasz zmienną składową message?

Jeśli chcesz uzyskać dostęp do odwołania do komponentu potomnego @ViewChild(ChildComponent), naprawdę musisz na niego poczekać ngAfterViewInit.

Brudną poprawką jest wywołanie updateMessage()kolejnej pętli zdarzeń za pomocą np. SetTimeout.

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}
Jo VdB
źródło
4
Zmieniłem mój kod na metodę ngOnInit.
Faliorn
29

W tym celu próbowałem powyżej odpowiedzi wiele nie działa w najnowszej wersji Angular (6 lub nowszy)

Korzystam z kontroli materiałów, która wymagała zmian po pierwszym wiązaniu.

    export class AbcClass implements OnInit, AfterContentChecked{
        constructor(private ref: ChangeDetectorRef) {}
        ngOnInit(){
            // your tasks
        }
        ngAfterContentChecked() {
            this.ref.detectChanges();
        }
    }

Dodając moją odpowiedź, to pomaga niektórym rozwiązać konkretny problem.

MarmiK
źródło
To faktycznie działało w moim przypadku, ale masz literówkę, po zaimplementowaniu AfterContentChecked powinieneś wywoływać ngAfterContentChecked, a nie ngAfterViewInit.
Tomas Lukac
Obecnie korzystam z wersji 8.2.0 :)
Tomas Lukac
1
@ TomášLukáč Dzięki za odpowiedź, zaktualizowałem swoją odpowiedź (y)
MarmiK
1
Polecam tę odpowiedź, jeśli żadne z powyższych nie działa.
Amir Choubani
Funkcja afterContentChecked jest wywoływana za każdym razem, gdy DOM jest ponownie obliczany lub sprawdzany ponownie. Intimating from Angular version 9
Imran Faruqi
24

Artykuł Wszystko, co musisz wiedzieć o ExpressionChangedAfterItHasBeenCheckedErrorbłędzie, wyjaśnia szczegółowo to zachowanie.

Problem z konfiguracją polega na tym, że ngAfterViewInithak cyklu życia jest wykonywany po przetworzeniu aktualizacji DOM przetworzonych zmian wykrywających zmiany. I skutecznie zmieniasz właściwość używaną w szablonie w tym haku, co oznacza, że ​​DOM musi zostać ponownie renderowany:

  ngAfterViewInit() {
    this.message = 'all done loading :)'; // needs to be rendered the DOM
  }

i będzie to wymagało kolejnego cyklu wykrywania zmian, a Angular by design uruchamia tylko jeden cykl podsumowania.

Zasadniczo masz dwie alternatywy, jak to naprawić:

  • zaktualizuj właściwość asynchronicznie za pomocą setTimeout , Promise.thenalbo asynchroniczny obserwowalne mowa w szablonie

  • przed aktualizacją DOM wykonaj aktualizację właściwości w trybie hook - ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked.

Max Koretskyi
źródło
Przeczytaj artykuły: blog.angularindepth.com/... , wkrótce przeczytam kolejny blog.angularindepth.com/... Nadal nie mogę znaleźć rozwiązania tego problemu. czy możesz mi powiedzieć, co się stanie, jeśli dodam hak cyklu życia ngDoCheck lub ngAfterContentChecked i dodam this.cdr.markForCheck (); (cdr dla ChangeDetectorRef) w nim. Czy to nie jest właściwy sposób sprawdzania zmian po zakończeniu cyklu życia i kolejnej kontroli.
Harsimer
9

Musisz tylko zaktualizować wiadomość w odpowiednim haku cyklu życia, w tym przypadku jest to ngAfterContentCheckedzamiast ngAfterViewInit, ponieważ w ngAfterViewInit sprawdzanie wiadomości zmiennej zostało rozpoczęte, ale jeszcze się nie zakończyło.

widzieć: https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#afterview

więc kod będzie po prostu:

import { Component } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message: string = 'loading :(';

  ngAfterContentChecked() {
     this.message = 'all done loading :)'
  }      
}

zobacz działające demo na Plunker.

RedPelle
źródło
Użyłem kombinacji @ViewChildren()licznika opartego na długości, który został zapełniony przez Observable. To było jedyne rozwiązanie, które działało dla mnie!
msanford,
2
Kombinacja z góry ngAfterContentCheckediz ChangeDetectorRefgóry działała dla mnie. On ngAfterContentCheckednazywa -this.cdr.detectChanges();
Kunal Dethe
7

Ten błąd nadchodzi, ponieważ istniejąca wartość jest aktualizowana natychmiast po zainicjowaniu. Jeśli więc zaktualizujesz nową wartość po wyrenderowaniu istniejącej wartości w DOM, to będzie działać dobrze. Podobnie jak wspomniano w tym artykule Debugowanie kątowe „Wyrażenie zmieniło się po sprawdzeniu”

na przykład możesz użyć

ngOnInit() {
    setTimeout(() => {
      //code for your new value.
    });

}

lub

ngAfterViewInit() {
  this.paginator.page
      .pipe(
          startWith(null),
          delay(0),
          tap(() => this.dataSource.loadLessons(...))
      ).subscribe();
}

Jak widać nie wspomniałem o czasie w metodzie setTimeout. Ponieważ jest to interfejs API dostarczany przez przeglądarkę, a nie interfejs API JavaScript, będzie on działał osobno na stosie przeglądarki i będzie czekał, aż elementy stosu wywołań zostaną zakończone.

W jaki sposób interfejs API przeglądarki wywołuje koncepcję Philipa Robertsa w jednym z filmów na Youtube (Czym jest hack, pętla zdarzeń?).

Sharad Jain
źródło
4

Możesz również umieścić swoje wywołanie updateMessage () w ngOnInt () - Metoda, przynajmniej mi to działa

ngOnInit() {
    this.updateMessage();
}

W RC1 nie powoduje to wyjątku

Tobias Gassmann
źródło
3

Możesz również utworzyć licznik czasu za pomocą Observable.timerfunkcji rxjs , a następnie zaktualizować wiadomość w swojej subskrypcji:                    

Observable.timer(1).subscribe(()=> this.updateMessage());
rabinneslo
źródło
2

Zgłasza błąd, ponieważ kod jest aktualizowany po wywołaniu funkcji ngAfterViewInit () . Oznacza to, że twoja wartość początkowa uległa zmianie, gdy ma miejsce ngAfterViewInit. Jeśli wywołasz to w ngAfterContentInit (), nie spowoduje to błędu.

ngAfterContentInit() {
    this.updateMessage();
}
Sandip - programista z pełnym stosem
źródło
0

Nie mogłem skomentować posta @Biranchi, ponieważ nie mam wystarczającej reputacji, ale rozwiązało to problem.

Jedną rzecz do zapamiętania! Jeśli dodajesz changeDetection: ChangeDetectionStrategy.OnPush na komponencie nie działał, a jego komponent potomny (głupi komponent) spróbuj również dodać go do elementu nadrzędnego.

To naprawiło błąd, ale zastanawiam się, jakie są tego skutki uboczne.

TKDev
źródło
0

Mam podobny błąd podczas pracy z danymi. Dzieje się tak, gdy używasz * ngFor w innym * ngForable danych rzucić ten błąd, ponieważ przerywa cykl zmiany kąta. SO zamiast korzystać z bazy danych wewnątrz bazy danych, użyj jednej zwykłej tabeli lub zamień mf.data na nazwę tablicy. To działa dobrze.

Priyanka Arora
źródło
0

Myślę, że najprostszym rozwiązaniem byłoby:

  1. Wykonaj jedną implementację przypisania wartości do jakiejś zmiennej, np. Za pomocą funkcji lub ustawiacza.
  2. Utwórz zmienną klasową (static working: boolean)w klasie, w której ta funkcja istnieje i za każdym razem, gdy wywołujesz funkcję, po prostu spraw, aby była prawdziwa, zależnie od tego, co chcesz. W ramach funkcji, jeśli wartość pracy jest prawdziwa, po prostu wróć od razu bez robienia czegokolwiek. w przeciwnym razie wykonaj żądane zadanie. Pamiętaj, aby zmienić tę zmienną na false po zakończeniu zadania, tj. Na końcu wiersza kodów lub w metodzie subskrypcji, gdy skończysz przypisywanie wartości!
Imran Faruqi
źródło