L'espressione ___ è cambiata dopo essere stata controllata


288

Perché è il componente in questo semplice plunk

@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 :)'
  }
}

lancio:

ECCEZIONE: l'espressione 'I'm {{message}} in App @ 0: 5' è cambiata dopo che è stata controllata. Valore precedente: 'Sto caricando :('. Valore corrente: 'Ho finito il caricamento :)' in [Sono {{message}} in App @ 0: 5]

quando tutto quello che sto facendo è aggiornare una semplice associazione quando viene avviata la mia vista?



Considera di modificare il tuo ChangeDetectionStrategyquando usi detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas,

Basti pensare che esiste un controllo di input e si stanno popolando i dati in un metodo e nello stesso metodo si sta assegnando un valore ad esso. Il compilatore verrà sicuramente confuso con il valore nuovo / precedente. Quindi il legame e il popolamento dovrebbero avvenire con metodi diversi.
MAC

Risposte:


295

Come affermato da drewmoore, la soluzione corretta in questo caso è attivare manualmente il rilevamento delle modifiche per il componente corrente. Questo viene fatto utilizzando il detectChanges()metodo ChangeDetectorRefdell'oggetto (importato da angular2/core) o il suo markForCheck()metodo, che consente anche l'aggiornamento di qualsiasi componente padre. Esempio pertinente :

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

}

Qui ci sono anche Plunkers che dimostrano gli approcci ngOnInit , setTimeout e enableProdMode per ogni evenienza.


7
Nel mio caso stavo aprendo un modale. Dopo aver aperto il modale stava mostrando il messaggio "L'espressione ___ è cambiata dopo che è stata controllata", quindi la mia soluzione è stata aggiunta this.cdr.detectChanges (); dopo aver aperto il mio modale. Grazie!
Jorge Casariego,

Dov'è la tua dichiarazione per la proprietà cdr? Mi aspetto di vedere una linea simile cdr : anyo qualcosa del genere sotto la messagedichiarazione. Sei solo preoccupato di aver perso qualcosa?
CodeCabbie

1
@CodeCabbie è negli argomenti del costruttore.
slasky

1
Questa risoluzione risolve il mio problema! Molte grazie! Modo molto chiaro e semplice.
lwozniak,

3
Questo mi ha aiutato, con alcune modifiche. Ho dovuto progettare elementi li generati tramite ngFor loop. Avevo bisogno di cambiare il colore degli span all'interno degli elementi dell'elenco in base a innerText facendo clic su 'ordina' che aggiornava un bool usato come parametro di una pipe di ordinamento (il risultato ordinato era una copia dei dati, quindi gli stili non stavano ottenendo aggiornato con ngStyle da solo). - Invece di utilizzare 'AfterViewInit', ho usato 'AfterViewChecked' - Mi sono anche assicurato di importare e implementare AfterViewChecked. nota: l'impostazione della pipe su 'pure: false' non funzionava, ho dovuto aggiungere questo passaggio extra (:
Chloe Corrigan,

171

Innanzitutto, tieni presente che questa eccezione verrà generata solo quando esegui la tua app in modalità dev (che è il caso per impostazione predefinita a partire da beta-0): se chiami enableProdMode()quando esegui il bootstrap dell'app, non verrà generata ( vedi plunk aggiornato ).

In secondo luogo, non farlo perché questa eccezione viene generata per una buona ragione: in breve, quando si è in modalità dev, ogni round di rilevamento delle modifiche è immediatamente seguito da un secondo round che verifica che nessun binding sia cambiato dalla fine del primo, poiché ciò indicherebbe che i cambiamenti sono causati dal rilevamento stesso del cambiamento.

Nel plunk, l'associazione {{message}}viene modificata dalla chiamata a setMessage(), che avviene in modalità ngAfterViewInithook, che si verifica come parte del turno di rilevamento delle modifiche iniziali. Ciò di per sé non è problematico, il problema è che setMessage()cambia l'associazione ma non attiva un nuovo ciclo di rilevamento delle modifiche, il che significa che questo cambiamento non verrà rilevato fino a quando non verrà attivato da qualche altra parte il futuro ciclo di rilevamento delle modifiche.

Il takeaway: tutto ciò che cambia un'associazione deve attivare un round di rilevamento delle modifiche quando lo fa.

Aggiorna in risposta a tutte le richieste per un esempio di come farlo : la soluzione di @ Tycho funziona, come fanno i tre metodi nella risposta @MarkRajcok. Ma francamente, si sentono tutti brutti e sbagliati per me, come il tipo di hack a cui ci siamo abituati appoggiandoci in ng1.

A dire il vero, ci sono circostanze occasionali in cui questi hack sono appropriati, ma se li stai usando su qualcosa di più di una base molto occasionale, è un segno che stai combattendo il framework piuttosto che abbracciare completamente la sua natura reattiva.

IMHO, un modo più idiomatico di "Angular2" per avvicinarsi a questo è qualcosa sulla falsariga di: ( 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 :)')
  }
}

13
Perché setMessage () non attiva un nuovo ciclo di rilevamento delle modifiche? Pensavo che Angular 2 attivasse automaticamente il rilevamento delle modifiche quando si cambia il valore di qualcosa nell'interfaccia utente.
Danny Libin,

4
@drewmoore "Tutto ciò che cambia un'associazione deve attivare un round di rilevamento delle modifiche quando lo fa". Come? È una buona pratica? Non dovrebbe essere completato tutto in una sola corsa?
Daniel Birowsky Popeski il

2
@Tycho, davvero. Da quando ho scritto quel commento, ho risposto a un'altra domanda in cui descrivo i 3 modi per eseguire il rilevamento delle modifiche , che include detectChanges().
Mark Rajcok,

4
solo per notare che, nell'attuale corpo di domanda, viene chiamato il metodo invocato updateMessage, nonsetMessage
superjos

3
@Daynil, ho avuto la stessa sensazione, fino a quando non ho letto il blog dato in commento sotto la domanda: blog.angularindepth.com/… Spiega perché questo deve essere fatto manualmente. In questo caso, il rilevamento delle modifiche di Angular ha un ciclo di vita. Nel caso in cui qualcosa stia cambiando un valore tra questi cicli di vita, è necessario eseguire un rilevamento forzato delle modifiche (o un settimeout - che viene eseguito nel successivo ciclo di eventi, attivando nuovamente il rilevamento delle modifiche).
Mahesh,

51

ngAfterViewChecked() ha funzionato per me:

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

constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
   //your code to update the model
   this.cdr.detectChanges();
}

Come accennato, il ciclo di rilevamento del cambiamento dell'angolo che è di due fasi, deve rilevare i cambiamenti dopo che la vista del bambino è stata modificata, e ritengo che il modo migliore per farlo sia quello di utilizzare il gancio del ciclo di vita fornito dall'angolo stesso e chiedere manualmente all'angolo di rilevare la modifica e associarla. La mia opinione personale è che questa sembra una risposta appropriata.
Joey587,

1
Questo lavoro per me, per caricare il componente dinamico della gerarchia all'interno insieme.
Mehdi Daustany,

47

Ho risolto questo problema aggiungendo ChangeDetectionStrategy dal nucleo angolare.

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})

Questo ha funzionato per me. Mi chiedo qual è la differenza tra questo e l'utilizzo di ChangeDetectorRef
Ari

1
Hmm ... Quindi la modalità del rilevatore di modifiche verrà inizialmente impostata su CheckOnce( documentazione )
Alex Klaus

Sì, l'errore / avviso è sparito. Ma ci vuole molto tempo di caricamento rispetto a prima come una differenza di 5-7 secondi che è enorme.
Kapil Raghuwanshi,

1
@KapilRaghuwanshi Esegui this.cdr.detectChanges();dopo tutto ciò che stai cercando di caricare. Perché potrebbe essere perché il rilevamento delle modifiche non si
attiva

36

Non puoi usare ngOnInitperché hai appena cambiato la variabile membro message?

Se si desidera accedere a un riferimento a un componente figlio @ViewChild(ChildComponent), è necessario attendere con esso ngAfterViewInit.

Una soluzione sporca è quella di chiamare il updateMessage()nel prossimo ciclo di eventi con es. SetTimeout.

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}

4
La modifica del mio codice nel metodo ngOnInit ha funzionato per me.
Faliorn,

29

Per questo ho provato sopra le risposte che molti non funzionano nell'ultima versione di Angular (6 o successive)

Sto usando il controllo materiale che ha richiesto modifiche dopo la prima rilegatura.

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

Aggiungendo la mia risposta così, questo aiuta alcuni a risolvere un problema specifico.


Questo in realtà ha funzionato per il mio caso, ma hai un refuso, dopo aver implementato AfterContentChecked dovresti chiamare ngAfterContentChecked, non ngAfterViewInit.
Tomas Lukac,

Sono attualmente alla versione 8.2.0 :)
Tomas Lukac il

1
@ TomášLukáč Grazie per la risposta, ho aggiornato la mia risposta (y)
MarmiK

1
Consiglio questa risposta se nessuna delle precedenti non funziona.
Amir Choubani,

afterContentChecked viene chiamato ogni volta che il DOM viene ricalcolato o ricontrollato. Intimating from Angular version 9
Imran Faruqi

24

L'articolo Tutto ciò che devi sapere ExpressionChangedAfterItHasBeenCheckedErrorsull'errore spiega il comportamento in modo dettagliato.

Il problema con la configurazione è che l' ngAfterViewInithook del ciclo di vita viene eseguito dopo gli aggiornamenti DOM elaborati per il rilevamento delle modifiche. E stai cambiando in modo efficace la proprietà utilizzata nel modello in questo hook, il che significa che DOM deve essere ridistribuito:

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

e ciò richiederà un altro ciclo di rilevamento delle modifiche e Angular by design esegue solo un ciclo digest.

Fondamentalmente hai due alternative su come risolverlo:

  • aggiornare la proprietà asincrono sia usando setTimeout, Promise.theno asincrona osservabile fatto riferimento nel modello

  • eseguire l'aggiornamento della proprietà in un hook prima dell'aggiornamento DOM: ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked.


Leggi i tuoi articoli: blog.angularindepth.com/… , Presto leggerò un altro blog.angularindepth.com/… . Non sono ancora riuscito a trovare una soluzione a questo problema. puoi dirmi cosa succede se aggiungo ngDoCheck o ngAfterContentChecked hook del ciclo di vita e aggiungo questo this.cdr.markForCheck (); (cdr per ChangeDetectorRef) al suo interno. Non è un modo corretto per verificare la presenza di modifiche dopo l'hook del ciclo di vita e il completamento del controllo successivo.
Harsimer

9

Devi solo aggiornare il tuo messaggio nel giusto hook del ciclo di vita, in questo caso ngAfterContentCheckedinvece che ngAfterViewInit, perché in ngAfterViewInit è stato avviato un controllo per il messaggio variabile ma non è ancora terminato.

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

quindi il codice sarà solo:

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 :)'
  }      
}

guarda la demo funzionante su Plunker.


Stavo usando una combinazione di un @ViewChildren()contatore basato sulla lunghezza che era popolato da un osservabile. Questa è stata l'unica soluzione che ha funzionato per me!
msanford,

2
La combinazione di ngAfterContentCheckede ChangeDetectorRefdall'alto ha funzionato per me. Su ngAfterContentCheckedchiamato -this.cdr.detectChanges();
Kunal Dethe

7

Questo errore sta arrivando perché il valore esistente viene aggiornato immediatamente dopo l'inizializzazione. Quindi, se aggiornerai un nuovo valore dopo che il valore esistente è stato reso in DOM, allora funzionerà bene. Come menzionato in questo articolo Debugging angolare "L'espressione è cambiata dopo che è stata controllata"

per esempio puoi usare

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

}

o

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

Come puoi vedere non ho menzionato il tempo nel metodo setTimeout. Poiché è un'API fornita dal browser, non un'API JavaScript, quindi verrà eseguita separatamente nello stack del browser e attenderà il completamento degli elementi dello stack delle chiamate.

Come l'API del browser invoca il concetto è spiegato da Philip Roberts in uno dei video di Youtube (Cos'è l'hacking il loop degli eventi?).


4

Puoi anche inviare la tua chiamata a updateMessage () nel metodo ngOnInt (), almeno funziona per me

ngOnInit() {
    this.updateMessage();
}

In RC1 questo non attiva l'eccezione


3

È inoltre possibile creare un timer utilizzando la Observable.timerfunzione rxjs e quindi aggiornare il messaggio nell'abbonamento :                    

Observable.timer(1).subscribe(()=> this.updateMessage());

2

Viene generato un errore perché il codice viene aggiornato quando viene chiamato ngAfterViewInit () . Significa che il valore iniziale è stato modificato quando si verifica ngAfterViewInit, se lo si chiama in ngAfterContentInit (), non verrà generato un errore.

ngAfterContentInit() {
    this.updateMessage();
}

0

Non ho potuto commentare il post di @Biranchi poiché non ho abbastanza reputazione, ma mi ha risolto il problema.

Una cosa da notare! Se aggiungi changeDetection: ChangeDetectionStrategy.OnPush sul componente non ha funzionato, ed è un componente figlio (componente stupido) prova ad aggiungerlo anche al genitore.

Questo risolto il bug, ma mi chiedo quali sono gli effetti collaterali di questo.


0

Ho avuto un errore simile mentre lavoravo con datatable. Quello che succede è quando usi * ngPer all'interno di un altro * ngPer datatable lanciare questo errore mentre intercetta il ciclo di cambio angolare. SO invece di usare datatable dentro datatable usa una tabella normale o sostituisci mf.data con il nome dell'array. Funziona benissimo.


0

Penso che una soluzione molto semplice sarebbe la seguente:

  1. Effettuare un'implementazione dell'assegnazione di un valore a qualche variabile, ad esempio tramite funzione o setter.
  2. Crea una variabile di classe (static working: boolean)nella classe in cui esiste questa funzione e ogni volta che la chiami, rendila vera come preferisci. All'interno della funzione, se il valore del lavoro è vero, allora semplicemente ritorna subito senza fare nulla. altrimenti, esegui l'attività che desideri. Assicurati di cambiare questa variabile in false una volta completata l'attività, ovvero alla fine della riga di codici o nel metodo di iscrizione quando hai finito di assegnare valori!
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.