Bagaimana mengelola Angular2 "ekspresi telah berubah setelah diperiksa" pengecualian ketika properti komponen tergantung pada datetime saat ini

168

Komponen saya memiliki gaya yang bergantung pada datetime saat ini. Dalam komponen saya, saya punya fungsi berikut.

  private fontColor( dto : Dto ) : string {
    // date d'exécution du dto
    let dtoDate : Date = new Date( dto.LastExecution );

    (...)

    let color =  "hsl( " + hue + ", 80%, " + (maxLigness - lightnessAmp) + "%)";

    return color;
  }

lightnessAmpdihitung dari datetime saat ini. Warnanya berubah jika dtoDatedalam 24 jam terakhir.

Kesalahan yang tepat adalah sebagai berikut:

Ekspresi telah berubah setelah diperiksa. Nilai sebelumnya: 'hsl (123, 80%, 49%)'. Nilai sekarang: 'hsl (123, 80%, 48%)'

Saya tahu pengecualian muncul dalam mode pengembangan hanya saat nilai diperiksa. Jika nilai yang dicentang berbeda dari nilai yang diperbarui, pengecualian dilemparkan.

Jadi saya mencoba memperbarui datetime saat ini di setiap siklus hidup dalam metode kait berikut untuk mencegah pengecualian:

  ngAfterViewChecked()
  {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
  }

... tetapi tanpa hasil.

Anthony Brenelière
sumber

Jawaban:

357

Jalankan deteksi perubahan secara eksplisit setelah perubahan:

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

constructor(private cdRef:ChangeDetectorRef) {}

ngAfterViewChecked()
{
  console.log( "! changement de la date du composant !" );
  this.dateNow = new Date();
  this.cdRef.detectChanges();
}
Günter Zöchbauer
sumber
Solusi sempurna, terima kasih. Saya perhatikan ini juga berfungsi dengan metode kait berikut: ngOnChanges, ngDoCheck, ngAfterContentChecked. Jadi, apakah ada opsi terbaik?
Anthony Brenelière
27
Itu tergantung pada kasus penggunaan Anda. Jika Anda ingin melakukan sesuatu ketika komponen diinisialisasi, ngOnInit()biasanya tempat pertama. Jika kode tergantung pada DOM yang diberikan ngAfterViewInit()atau ngAfterContentInit()merupakan opsi berikutnya. ngOnChanges()cocok jika kode harus dieksekusi setiap kali input diubah. ngDoCheck()adalah untuk deteksi perubahan kustom. Sebenarnya saya tidak tahu ngAfterViewChecked()digunakan untuk apa. Saya pikir itu disebut sebelum atau sesudah ngAfterViewInit().
Günter Zöchbauer
2
@KushalJayswal maaf, tidak masuk akal dari deskripsi Anda. Saya sarankan untuk membuat pertanyaan baru dengan kode yang menunjukkan apa yang ingin Anda capai. Idealnya dengan contoh StackBlitz.
Günter Zöchbauer
4
Ini juga merupakan solusi yang sangat baik jika keadaan komponen Anda didasarkan pada properti DOM yang dihitung-peramban, seperti clientWidth, dll.
Jonah
1
Hanya digunakan pada ngAfterViewInit, bekerja seperti pesona.
Francisco Arleo
42

TL; DR

ngAfterViewInit() {
    setTimeout(() => {
        this.dateNow = new Date();
    });
}

Meskipun ini merupakan solusi, kadang-kadang sangat sulit untuk menyelesaikan masalah ini dengan cara yang lebih baik, jadi jangan salahkan diri Anda jika Anda menggunakan pendekatan ini. Tidak apa-apa.

Contoh : Masalah awal [ tautan ], Diselesaikan dengan setTimeout()[ tautan ]


Bagaimana cara menghindarinya

Secara umum kesalahan ini biasanya terjadi setelah Anda menambahkan suatu tempat (bahkan dalam komponen induk / anak) ngAfterViewInit. Jadi pertanyaan pertama adalah bertanya pada diri sendiri - dapatkah saya hidup tanpanya ngAfterViewInit? Mungkin Anda memindahkan kode di suatu tempat ( ngAfterViewCheckedmungkin menjadi alternatif).

Contoh : [ tautan ]


Juga

Juga hal-hal async ngAfterViewInityang mempengaruhi DOM dapat menyebabkan ini. Juga dapat diselesaikan melalui setTimeoutatau dengan menambahkan delay(0)operator di dalam pipa:

ngAfterViewInit() {
  this.foo$
    .pipe(delay(0)) //"delay" here is an alternative to setTimeout()
    .subscribe();
}

Contoh : [ tautan ]


Bacaan yang bagus

Artikel bagus tentang cara men-debug ini dan mengapa itu terjadi: tautan

Sergei Panfilov
sumber
2
Tampaknya lebih lambat daripada solusi yang dipilih
Pete B
6
Ini bukan solusi terbaik, tapi sial ini SELALU bekerja. Jawaban yang dipilih tidak selalu berhasil (orang perlu memahami hook dengan cukup baik untuk membuatnya bekerja)
Freddy Bonda
Apa penjelasan yang benar mengapa ini berhasil, ada yang tahu? Apakah itu karena ia kemudian berjalan di Thread yang berbeda (async)?
knnhcn
3
@knnhcn Tidak ada utas berbeda dalam javascript. JS bersifat single-threaded secara alami. SetTimeout hanya memberi tahu mesin untuk menjalankan fungsi beberapa saat setelah penghitung waktu berakhir. Di sini timernya adalah 0, yang sebenarnya diperlakukan sebagai 4 di browser modern, yang banyak waktu untuk sudut melakukan hal-hal ajaibnya: developer.mozilla.org/en-US/docs/Web/API/…
Kilves
27

Seperti yang disebutkan oleh @leocaseiro tentang masalah github .

Saya menemukan 3 solusi untuk mereka yang mencari perbaikan mudah.

1) Pindah dari ngAfterViewInitkengAfterContentInit

2) Pindah ke ngAfterViewCheckeddigabungkan dengan ChangeDetectorRefseperti yang disarankan pada # 14748 (komentar)

3) Tetap menggunakan ngOnInit () tetapi panggil ChangeDetectorRef.detectChanges()setelah perubahan Anda.

jujur
sumber
1
Adakah yang bisa mendokumentasikan solusi apa yang paling direkomendasikan?
Pipo
13

Dia Anda pergi dua solusi!

1. Ubah ChangeDetectionStrategy ke OnPush

Untuk solusi ini, Anda pada dasarnya memberi tahu sudut:

Berhenti memeriksa perubahan; Saya akan melakukannya hanya jika saya tahu itu perlu

Perbaikan cepat:

Ubah komponen Anda sehingga akan digunakan ChangeDetectionStrategy.OnPush

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
    // ...
}

Dengan ini, semuanya sepertinya tidak berfungsi lagi. Itu karena mulai sekarang Anda harus membuat panggilan Angular detectChanges()secara manual.

this.cdr.detectChanges();

Berikut ini tautan yang membantu saya memahami ChangeDetectionStrategy benar: https://alligator.io/angular/change-detection-strategy/

2. Memahami Ekspresi yang DiubahSetelahMemilikiBeenTidak Teror

Berikut ini adalah ekstrak kecil dari jawaban tomonari_t tentang penyebab kesalahan ini, saya hanya mencoba memasukkan bagian-bagian yang membantu saya untuk memahami hal ini.

Artikel lengkap menunjukkan contoh kode nyata tentang setiap titik yang ditampilkan di sini.

Penyebab dasarnya adalah siklus hidup sudut:

Setelah setiap operasi, Angular ingat nilai apa yang digunakannya untuk melakukan operasi. Mereka disimpan di properti oldValues ​​dari tampilan komponen.

Setelah pemeriksaan dilakukan untuk semua komponen, Angular kemudian memulai siklus intisari berikutnya, tetapi alih-alih melakukan operasi, ia membandingkan nilai saat ini dengan yang ia ingat dari siklus intisari sebelumnya.

Operasi berikut sedang diperiksa pada siklus digest:

periksa bahwa nilai yang diturunkan ke komponen turunan sama dengan nilai yang akan digunakan untuk memperbarui properti komponen ini sekarang.

periksa bahwa nilai yang digunakan untuk memperbarui elemen DOM sama dengan nilai yang akan digunakan untuk memperbarui elemen ini sekarang melakukan hal yang sama.

memeriksa semua komponen anak

Jadi, kesalahan dilemparkan ketika nilai yang dibandingkan berbeda. , blogger Max Koretskyi menyatakan:

Pelakunya selalu merupakan komponen anak atau arahan.

Dan akhirnya, inilah beberapa contoh dunia nyata yang biasanya menyebabkan kesalahan ini:

  • Layanan berbagi
  • Siaran acara sinkron
  • Instansiasi komponen dinamis

Setiap sampel dapat ditemukan di sini (plunkr), dalam kasus saya masalahnya adalah instantiasi komponen dinamis.

Juga, berdasarkan pengalaman saya sendiri, saya sangat menyarankan semua orang untuk menghindari setTimeoutsolusi, dalam kasus saya menyebabkan loop "hampir" tak terbatas (21 panggilan yang saya tidak mau tunjukkan kepada Anda bagaimana memprovokasi mereka),

Saya akan merekomendasikan untuk selalu mengingat siklus hidup Angular sehingga Anda dapat memperhitungkan bagaimana mereka akan terpengaruh setiap kali Anda mengubah nilai komponen lain. Dengan kesalahan ini, Angular memberi tahu Anda:

Anda mungkin melakukan ini dengan cara yang salah, apakah Anda yakin Anda benar?

Blog yang sama juga mengatakan:

Seringkali, perbaikannya adalah dengan menggunakan kait deteksi perubahan yang tepat untuk membuat komponen dinamis

Panduan singkat bagi saya adalah untuk mempertimbangkan setidaknya dua hal berikut ini saat coding ( saya akan mencoba melengkapinya dari waktu ke waktu ):

  1. Hindari mengubah nilai komponen induk dari komponen turunannya, sebagai gantinya: modifikasi dari induknya.
  2. Ketika Anda menggunakan @Inputdan @Outputarahan, cobalah untuk menghindari memicu perubahan siklus lyf kecuali komponen sepenuhnya diinisialisasi.
  3. Hindari panggilan yang tidak perlu this.cdr.detectChanges();karena dapat memicu lebih banyak kesalahan, khususnya ketika Anda berurusan dengan banyak data dinamis
  4. Ketika penggunaan this.cdr.detectChanges();wajib, pastikan bahwa variabel ( @Input, @Output, etc) yang digunakan diisi / diinisialisasi pada kait deteksi kanan ( OnInit, OnChanges, AfterView, etc)
  5. Jika memungkinkan, hapus alih-alih sembunyikan , ini terkait dengan poin 3 dan 4.

Juga

Jika Anda ingin sepenuhnya memahami Angular Life Hook, saya sarankan Anda untuk membaca dokumentasi resmi di sini:

Luis Limas
sumber
Saya masih tidak mengerti mengapa mengubah ChangeDetectionStrategyuntuk OnPushmemperbaikinya bagi saya. Saya memiliki komponen sederhana, yang memiliki [disabled]="isLastPage()". Metode itu membaca MatPaginator-ViewChild dan kembali this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Paginator tidak tersedia segera, tapi setelah itu telah terikat dengan menggunakan @ViewChild. Mengubah ChangeDetectionStrategykesalahan yang dihapus - dan fungsi masih ada seperti sebelumnya. Tidak yakin kekurangan yang saya miliki sekarang, tapi terima kasih!
Igor
Itu bagus @Igor! satu-satunya penarikan menggunakan OnPushadalah Anda harus menggunakan this.cdr.detectChanges()setiap kali Anda ingin komponen Anda untuk menyegarkan. Saya kira Anda sudah menggunakannya
Luis Limas
9

Dalam kasus kami, kami TETAP dengan menambahkan changeDetection ke dalam komponen dan memanggil detectChanges () di ngAfterContentChecked, kode sebagai berikut

@Component({
  selector: 'app-spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpinnerComponent implements OnInit, OnDestroy, AfterContentChecked {

  show = false;

  private subscription: Subscription;

  constructor(private spinnerService: SpinnerService, private changeDedectionRef: ChangeDetectorRef) { }

  ngOnInit() {
    this.subscription = this.spinnerService.spinnerState
      .subscribe((state: SpinnerState) => {
        this.show = state.show;
      });
  }

  ngAfterContentChecked(): void {
      this.changeDedectionRef.detectChanges();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}
Charitha Goonewardena
sumber
2

Saya pikir solusi terbaik dan terbersih yang dapat Anda bayangkan adalah ini:

@Component( {
  selector: 'app-my-component',
  template: `<p>{{ myData?.anyfield }}</p>`,
  styles: [ '' ]
} )
export class MyComponent implements OnInit {
  private myData;

  constructor( private myService: MyService ) { }

  ngOnInit( ) {
    /* 
      async .. await 
      clears the ExpressionChangedAfterItHasBeenCheckedError exception.
    */
    this.myService.myObservable.subscribe(
      async (data) => { this.myData = await data }
    );
  }
}

Diuji dengan Angular 5.2.9

JavierFuentes
sumber
3
Ini adalah hacky .. dan jadi tidak perlu susu.
JoeriShoeby
1
@JoeriShoeby semua solusi lainnya di atas workarounds berdasarkan setTimeouts atau peristiwa sudut canggih ... solusi ini adalah ES2017 murni didukung oleh semua jurusan browser developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... caniuse.com / # search = menunggu
JavierFuentes
1
Bagaimana jika Anda membangun aplikasi seluler (menargetkan Android API 17) menggunakan Angular + Cordova misalnya, di mana Anda tidak dapat mengandalkan fitur ES2017? Perhatikan bahwa jawaban yang diterima adalah solusi, bukan solusi ..
JoeriShoeby
@ JoeriShoeby Menggunakan naskah, ini dikompilasi ke JS, jadi tidak ada masalah dukungan fitur.
biggbest
@ biggbest Apa maksudmu dengan itu? Saya tahu TypeScript akan dikompilasi ke JS, tetapi itu tidak membuat Anda dapat menganggap semua JS akan berfungsi. Perhatikan bahwa Javascript dijalankan di browser web, dan setiap browser berperilaku berbeda.
JoeriShoeby
2

Pekerjaan kecil di sekitar saya digunakan berkali-kali

Promise.resolve(null).then(() => {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
    this.cdRef.detectChanges();
});

Saya terutama mengganti "null" dengan beberapa variabel yang saya gunakan di controller.

Khateeb321
sumber
1

Meskipun sudah ada banyak jawaban dan tautan ke artikel yang sangat bagus tentang deteksi perubahan, saya ingin memberikan dua sen saya di sini. Saya pikir cek ada karena suatu alasan jadi saya berpikir tentang arsitektur aplikasi saya dan menyadari bahwa perubahan dalam tampilan dapat ditangani dengan menggunakan BehaviourSubjectdan kait siklus hidup yang benar. Jadi inilah yang saya lakukan untuk solusi.

  • Saya menggunakan komponen pihak ketiga ( kalender penuh) , tetapi saya juga menggunakan Bahan Angular , jadi meskipun saya membuat plugin baru untuk penataan, mendapatkan tampilan dan nuansa agak canggung karena kustomisasi tajuk kalender tidak dapat dilakukan tanpa mengotori repo. dan bergulir sendiri.
  • Jadi saya akhirnya mendapatkan kelas JavaScript yang mendasarinya, dan perlu menginisialisasi header kalender saya sendiri untuk komponen. Itu mengharuskan ViewChilduntuk dirender sebelum orang tua saya diberikan, yang bukan cara Angular bekerja. Inilah mengapa saya membungkus nilai yang saya butuhkan untuk template saya di BehaviourSubject<View>(null):

    calendarView$ = new BehaviorSubject<View>(null);

Selanjutnya, ketika saya bisa memastikan tampilan dicentang, saya memperbarui subjek itu dengan nilai dari @ViewChild:

  ngAfterViewInit(): void {
    // ViewChild is available here, so get the JS API
    this.calendarApi = this.calendar.getApi();
  }

  ngAfterViewChecked(): void {
    // The view has been checked and I know that the View object from
    // fullcalendar is available, so emit it.
    this.calendarView$.next(this.calendarApi.view);
  }

Kemudian, dalam templat saya, saya hanya menggunakan asyncpipa. Tidak ada peretasan dengan deteksi perubahan, tidak ada kesalahan, bekerja dengan lancar.

Harap jangan ragu untuk bertanya apakah Anda membutuhkan detail lebih lanjut.

thomi
sumber
1

Gunakan nilai formulir default untuk menghindari kesalahan.

Alih-alih menggunakan jawaban yang diterima dari menerapkan detectChanges () di ngAfterViewInit () (yang juga memecahkan kesalahan dalam kasus saya), saya malah memutuskan untuk menyimpan nilai default untuk bidang formulir yang diperlukan secara dinamis, sehingga ketika formulir tersebut kemudian diperbarui, validitasnya tidak berubah jika pengguna memutuskan untuk mengubah opsi pada formulir yang akan memicu bidang yang diperlukan baru (dan menyebabkan tombol kirim dinonaktifkan).

Ini menyimpan sedikit kode dalam komponen saya, dan dalam kasus saya kesalahannya dihindari sama sekali.

T. Bulford
sumber
Ini adalah praktik yang sangat baik, dengan ini Anda menghindari panggilan yang tidak perluthis.cdr.detectChanges()
Luis Limas
1

Saya mendapatkan kesalahan itu karena saya mendeklarasikan variabel dan kemudian ingin
mengubah nilainya menggunakanngAfterViewInit

export class SomeComponent {

    header: string;

}

untuk memperbaikinya saya beralih dari

ngAfterViewInit() { 

    // change variable value here...
}

untuk

ngAfterContentInit() {

    // change variable value here...
}
pengguna12163165
sumber