typescript - Expresia ___ s-a schimbat după ce a fost verificată
De ce este componenta în acest simplu 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 :)'
}
}
aruncare:
EXCEPȚIE: Expresia 'Sunt {{message}} în App@0:5' s-a schimbat după ce a fost verificată. Valoarea anterioară: 'Sunt în încărcare :( '. Valoarea curentă: 'Am terminat încărcarea :) ' în [Sunt {{message}} în App@0:5]
când tot ce fac este să actualizez o legătură simplă atunci când este inițiată vizualizarea mea?
Răspuns #1
Așa cum a afirmat drewmoore, soluția adecvată în acest caz este să declanșezi manual detectarea schimbărilor pentru componenta curentă. Acest lucru se face folosind metoda detectChanges() a obiectului ChangeDetectorRef (importat din angular2/core), sau metoda sa markForCheck(), care face de asemenea ca orice componente părinte să se actualizeze. Exemplu relevant:
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();
}
}
Aici sunt, de asemenea, Plunkers care demonstrează abordările ngOnInit, setTimeout și enableProdMode, doar în caz.
Răspunsul #2
Mai întâi, rețineți că această excepție va fi aruncată doar atunci când rulați aplicația în modul dev (cazul implicit începând cu beta-0): Dacă apelați enableProdMode() la inițializarea aplicației, nu va fi aruncată (vedeți plunk-ul actualizat).
În al doilea rând, nu face asta deoarece această excepție este aruncată dintr-un motiv întemeiat: Pe scurt, atunci când ești în mod dev, fiecare rundă de detectare a schimbărilor este urmată imediat de o a doua rundă care verifică ca nicio legătură să nu se fi schimbat de la sfârșitul primei, deoarece acest lucru ar indica faptul că schimbările sunt cauzate chiar de detectarea schimbărilor.
În plunkul tău, binding-ul {{message}} este schimbat de apelul tău la setMessage(), care are loc în hook-ul ngAfterViewInit, care apare ca parte a primei detectări a modificărilor. Aceasta, în sine, nu este problematică însă - problema este că setMessage() schimbă binding-ul, dar nu declanșează o nouă rundă de detectare a modificărilor, ceea ce înseamnă că această schimbare nu va fi detectată până când nu va fi declanșată o rundă viitoare de detectare a modificărilor în altă parte.
Concluzia: Orice lucru care schimbă o legătură trebuie să declanșeze o rundă de detectare a schimbărilor atunci când o face.
Actualizare ca răspuns la toate cererile pentru un exemplu despre cum să faci asta: Soluția lui @Tycho funcționează, la fel și cele trei metode menționate în răspuns de @MarkRajcok. Dar sincer, toate mi se par urâte și greșite, ca genul de trucuri la care ne-am obișnuit să apelăm în ng1.
Să fim siguri, există circumstanțe ocazionale în care aceste trucuri sunt potrivite, dar dacă le folosești mai des decât într-un mod foarte ocazional, este un semn că te lupți cu cadrul în loc să îmbrățișezi pe deplin natura sa reactivă.
După părerea mea, o abordare mai idiomatică, „în stilul Angular2” în această privință ar fi ceva de genul: (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 :)')
}
}
Răspunsul #3
ngAfterViewChecked() a funcționat pentru mine:
import { Component, ChangeDetectorRef } from '@angular/core'; //import ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
//your code to update the model
this.cdr.detectChanges();
}
Răspunsul #4
Am rezolvat asta prin adăugarea ChangeDetectionStrategy din Angular core.
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'page1',
templateUrl: 'page1.html',
})
Răspunsul #5
Nu poți folosi ngOnInit pentru că doar modifici variabila membră message?
Dacă vrei să accesezi o referință către un component copil @ViewChild(ChildComponent), într-adevăr trebuie să aștepți pentru asta cu ngAfterViewInit.
O rezolvare murdară este să apelăm updateMessage() în următoarea buclă de evenimente, de exemplu cu setTimeout.
ngAfterViewInit() {
setTimeout(() => {
this.updateMessage();
}, 1);
}
Răspunsul #6
Pentru asta am încercat răspunsurile de mai sus, multe nu funcționează în cea mai recentă versiune de Angular (6 sau mai recentă)
Folosesc controlul Material care a necesitat modificări după ce prima legătură a fost realizată.
export class AbcClass implements OnInit, AfterContentChecked{
constructor(private ref: ChangeDetectorRef) {}
ngOnInit(){
// your tasks
}
ngAfterContentChecked() {
this.ref.detectChanges();
}
}
Adăugând răspunsul meu, astfel, acest lucru ajută pe unii să rezolve o problemă specifică.
Răspuns #7
Am trecut de la AfterViewInit la AfterContentChecked și a funcționat pentru mine.
Iată procesul
Adaugă dependența în constructorul tău:
constructor (private cdr: ChangeDetectorRef) {}și apelează autentificarea ta în codul metodei implementate aici:
ngAfterContentChecked() { this.cdr.detectChanges(); // call or add here your code }
Răspuns #8
Articolul Tot ce trebuie să știi despre eroarea ExpressionChangedAfterItHasBeenCheckedError explică comportamentul în detaliu.
Problema cu configurația ta este că hook-ul de ciclu de viață ngAfterViewInit este executat după ce detecția de schimbări a procesat actualizările DOM. Și efectiv schimbi proprietatea care este utilizată în șablon în acest hook, ceea ce înseamnă că DOM-ul trebuie re-renderizat:
ngAfterViewInit() {
this.message = 'all done loading :)'; // needs to be rendered the DOM
}
și acest lucru va necesita un alt ciclu de detectare a modificărilor, iar Angular, prin design, rulează doar un singur ciclu de digestie.
Practic ai două alternative pentru a rezolva asta:
actualizează proprietatea asincron fie folosind
setTimeout,Promise.thensau un observable asincron referit în șablonefectuați actualizarea proprietății într-un hook înainte de actualizarea DOM-ului - ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked.
Răspunsul #9
Această eroare apare deoarece valoarea existentă este actualizată imediat după ce este inițializată. Așadar, dacă vei actualiza o valoare nouă după ce valoarea existentă a fost afișată în DOM, atunci va funcționa corect. Așa cum este menționat în acest articol Angular Debugging „Expression has changed after it was checked”
de exemplu poți folosi
ngOnInit() {
setTimeout(() => {
//code for your new value.
});
}
sau
ngAfterViewInit() {
this.paginator.page
.pipe(
startWith(null),
delay(0),
tap(() => this.dataSource.loadLessons(...))
).subscribe();
}
După cum puteți vedea, nu am menționat timpul în metoda setTimeout. Deoarece este o API oferită de browser, nu o API JavaScript, aceasta va rula separat în stiva browserului și va aștepta până când elementele din stiva de apeluri sunt finalizate.
Modul în care API-ul browserului invocă conceptul este explicat de Philip Roberts într-unul dintre videoclipurile de pe YouTube (What the hack is event loop?).
Răspunsul #10
Trebuie doar să-ți actualizezi mesajul în hook-ul corect al ciclului de viață, în acest caz este ngAfterContentChecked în loc de ngAfterViewInit, pentru că în ngAfterViewInit a început o verificare pentru variabila message, dar nu s-a încheiat încă.
vezi: https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#afterview
așadar codul va fi doar:
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 :)'
}
}
vedeți demonstrația funcțională pe Plunker.
