Bir bileşen özelliği geçerli tarih saatine bağlı olduğunda Angular2'nin "ifade denetlendikten sonra değişti" özel durumu nasıl yönetilir


168

Bileşenimin geçerli tarih saatine bağlı stilleri var. Bileşenimde aşağıdaki fonksiyon var.

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

lightnessAmp, geçerli tarih saatinden hesaplanır. dtoDateSon 24 saat içinde ise renk değişir .

Kesin hata şudur:

İfade kontrol edildikten sonra değişti. Önceki değer: 'hsl (123,% 80,% 49)'. Geçerli değer: 'hsl (123,% 80,% 48)'

İstisna sadece değer kontrol edildiğinde geliştirme modunda göründüğünü biliyorum. İşaretlenen değer güncellenen değerden farklıysa istisna atılır.

Bu nedenle, istisnayı önlemek için aşağıdaki kanca yönteminde her yaşam döngüsünde geçerli tarih saatini güncellemeye çalıştım:

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

... ama başarılı olamadı.


Yanıtlar:


357

Değişiklikten sonra değişiklik algılamayı açıkça çalıştırın:

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

Mükemmel çözüm, teşekkür ederim. Aşağıdaki kanca yöntemleri ile de çalıştığını fark ettim: ngOnChanges, ngDoCheck, ngAfterContentChecked. En iyi seçenek var mı?
Anthony Brenelière

28
Bu sizin kullanım durumunuza bağlıdır. Bir bileşen başlatıldığında bir şey yapmak istiyorsanız ngOnInit(), genellikle ilk sırada yer alır. Kod, görüntülenen DOM'ye bağlıysa ngAfterViewInit()veya bir ngAfterContentInit()sonraki seçenekse. ngOnChanges()bir giriş her değiştirildiğinde kodun yürütülmesi gerekiyorsa iyi bir seçimdir. ngDoCheck()özel değişiklik tespiti içindir. Aslında ngAfterViewChecked()en iyi ne için kullanıldığını bilmiyorum . Bence hemen önce ya da sonra denir ngAfterViewInit().
Günter Zöchbauer

2
@KushalJayswal üzgünüm, açıklamanızdan mantıklı olamaz. Yapmaya çalıştığınız şeyi gösteren kodla yeni bir soru oluşturmanızı öneririm. İdeal olarak StackBlitz örneği.
Günter Zöchbauer

4
Bileşen durumunuz gibi tarayıcı tarafından hesaplanan DOM özelliklerine clientWidthvb.
Jonah

1
Sadece ngAfterViewInit üzerinde kullanılır, bir cazibe gibi çalıştı.
Francisco Arleo

42

TL; DR

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

Bu geçici bir çözüm olsa da, bazen bu sorunu daha güzel bir şekilde çözmek gerçekten zordur, bu nedenle bu yaklaşımı kullanıyorsanız kendinizi suçlamayın. Sorun yok.

Örnekler : İlk sayı [ link ], setTimeout()[ link ] ile çözüldü


Nasıl önlenir

Genel olarak bu hata genellikle bir yere (üst / alt bileşenlerde bile) ekledikten sonra olur ngAfterViewInit. İlk soru kendinize sormaktır - onsuz yaşayabilir miyim ngAfterViewInit? Belki de kodu bir yere taşırsınız ( ngAfterViewCheckedalternatif olabilir).

Örnek : [ bağlantı ]


Ayrıca

Ayrıca ngAfterViewInitDOM'u etkileyen zaman uyumsuz şeyler buna neden olabilir. Ayrıca operatörün boruya setTimeouteklenmesi veya delay(0)operatörün boruya eklenmesi ile çözülebilir :

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

Örnek : [ bağlantı ]


Güzel Okuma

Bunu nasıl ayıklayacağınız ve neden olduğu hakkında iyi bir makale: link


2
Seçilen çözümden daha yavaş görünüyor
Pete B

6
Bu en iyi çözüm değil, ama bu HER ZAMAN işe yarıyor. Seçilen cevap her zaman işe yaramaz (kişinin çalışmasını sağlamak için kancayı oldukça iyi anlaması gerekir)
Freddy Bonda

Bunun neden işe yaradığını gösteren doğru açıklama nedir? Daha sonra farklı bir iş parçacığında (zaman uyumsuz) çalıştığı için mi?
knnhcn

3
@knnhcn Javascript'te farklı bir konu diye bir şey yoktur. JS doğası gereği tek iş parçacıklıdır. SetTimeout, motora zamanlayıcının süresi dolduktan bir süre sonra işlevi yürütmesini söyler . Burada zamanlayıcı 0, modern tarayıcılarda aslında 4 olarak değerlendiriliyor, açısal için sihirli şeyleri yapmak için çok zaman var: developer.mozilla.org/en-US/docs/Web/API/…
Kilves

27

Üzerinde @leocaseiro tarafından belirtildiği gibi github sorunu .

Kolay düzeltmeler arayanlar için 3 çözüm buldum.

1) taşıma ngAfterViewInitiçinngAfterContentInit

2) # 14748'de önerildiği gibi bir ngAfterViewCheckedaraya ChangeDetectorRefgetirme (yorum)

3) ngOnInit () ile devam edin, ancak ChangeDetectorRef.detectChanges()değişikliklerinizin ardından arayın .


1
Herkes en çok önerilen çözümün ne olduğunu belgeleyebilir mi?
Pipo

13

Ona iki çözüm var!

1. ChangeDetectionStrategy'i OnPush olarak değiştirin

Bu çözüm için temel olarak açısal olarak şunu söylersiniz:

Değişiklikleri kontrol etmeyi bırakın; bunu sadece gerekli olduğunu bildiğimde yapacağım

Hızlı düzeltme:

Bileşeninizi kullanacak şekilde değiştirin ChangeDetectionStrategy.OnPush

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

Bununla birlikte, işler artık işe yaramıyor gibi görünüyor. Çünkü bundan böyle detectChanges()manuel olarak Açısal çağrı yapmanız gerekecek .

this.cdr.detectChanges();

İşte ChangeDetectionStrategy'yi anlamama yardımcı olan bir bağlantı : https://alligator.io/angular/change-detection-strategy/

2. İfadeyi AnlamaDeğiştirilmişAfterItHasBeenCheckedError

İşte bu hatanın nedenleri hakkında tomonari_t cevabından küçük bir alıntı, sadece bunu anlamama yardımcı olan parçaları dahil etmeye çalıştım.

Makalenin tamamı burada gösterilen her noktaya ilişkin gerçek kod örneklerini göstermektedir.

Temel neden açısal yaşam döngüsünün:

Her işlemden sonra Angular bir işlemi gerçekleştirmek için hangi değerleri kullandığını hatırlar. Bileşenler görünümünün oldValues ​​özelliğinde saklanırlar.

Tüm bileşenler için kontroller yapıldıktan sonra Angular bir sonraki sindirim döngüsünü başlatır, ancak işlemleri gerçekleştirmek yerine mevcut değerleri bir önceki sindirim çevriminden hatırladığı değerlerle karşılaştırır.

Aşağıdaki işlemler özet döngüsünde kontrol ediliyor:

alt bileşenlere aktarılan değerlerin, bu bileşenlerin özelliklerini güncellemek için kullanılacak değerlerle aynı olduğunu kontrol edin.

DOM öğelerini güncellemek için kullanılan değerlerin, bu öğeleri güncellemek için kullanılacak değerlerle aynı olduğunu kontrol edin.

tüm alt bileşenler için kontroller

Ve böylece, karşılaştırılan değerler farklı olduğunda hata atılır. , blog yazarı Max Koretskyi şunları söyledi:

Suçlu daima alt öğedir veya bir direktiftir.

Ve son olarak, genellikle bu hataya neden olan bazı gerçek dünya örnekleri:

  • Paylaşılan hizmetler
  • Senkron olay yayını
  • Dinamik bileşen örneği

Her örnek burada bulunabilir (plunkr), benim durumumda sorun dinamik bir bileşen örneğiydi.

Ayrıca, kendi deneyimlerime göre, herkesin setTimeoutçözümü önlemek için şiddetle tavsiye ediyorum , benim durumumda "neredeyse" sonsuz bir döngüye neden oldu (bunları nasıl kışkırtmayı göstermeye istekli olmadığımı 21 çağrı),

Her zaman akılda tutmanızı tavsiye ederim Açısal yaşam döngüsü, böylece başka bir bileşenin değerini her değiştirdiğinizde nasıl etkileneceğini dikkate alabilirsiniz. Bu hata ile Angular size şunları söylüyor:

Belki de bunu yanlış yapıyorsun, haklı olduğuna emin misin?

Aynı blog şunları da söylüyor:

Genellikle düzeltme, dinamik bir bileşen oluşturmak için doğru değişiklik algılama kancasını kullanmaktır

Benim için kısa bir rehber kodlama sırasında en azından aşağıdaki iki şeyi düşünmektir ( zamanla tamamlamaya çalışacağım ):

  1. Üst bileşen değerlerini alt öğelerinin bileşenlerinde değiştirmekten kaçının, bunun yerine: üst öğeden değiştirin.
  2. Kullandığınızda @Inputve @Outputyönergeler, bileşen tamamen başlatılmadıkça lyfecycle değişikliklerini tetiklemekten kaçının.
  3. Gereksiz çağrılardan kaçının this.cdr.detectChanges(); , özellikle çok fazla dinamik veriyle uğraşırken daha fazla hatayı tetikleyebilirler
  4. Kullanımı this.cdr.detectChanges();zorunlu olduğunda, kullanılan değişkenlerin ( @Input, @Output, etc) sağ algılama kancasında ( OnInit, OnChanges, AfterView, etc) doldurulduğundan / başlatıldığından emin olun.
  5. Mümkün olduğunda, saklamak yerine kaldırın , bu 3. ve 4. nokta ile ilgilidir.

Ayrıca

Angular Life Hook'u tam olarak anlamak istiyorsanız, burada resmi belgeleri okumanızı tavsiye ederim:


Değişen Hala neden anlayamıyorum ChangeDetectionStrategyiçin OnPushbenim için sabit buna. Basit bir bileşenim var [disabled]="isLastPage()". Bu yöntem MatPaginator-ViewChild okuma ve döndürme this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Sayfalayıcı hemen kullanılamaz, ancak kullanımı sınırlandıktan sonra @ViewChild. Değiştirilmesi ChangeDetectionStrategyhatayı kaldırdı - ve işlevsellik hala daha önce olduğu gibi var. Şimdi hangi sakıncalarım olduğundan emin değilim, ama teşekkür ederim!
Igor

Bu harika @Igor! kullanmanın tek çekilmesi, bileşenin her yenilemesini istediğinizde OnPushkullanmanızdır this.cdr.detectChanges(). Sanırım zaten kullanıyordun
Luis Limas

9

Bizim durumumuzda, bileşene changeDetection ekleyerek DÜZELTİLDİ ve ngAfterContentChecked kodunda detChanges () yöntemini çağırın, aşağıdaki gibi kodlayın

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

}

2

Bence hayal edebileceğiniz en iyi ve en temiz çözüm şudur:

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

Açısal 5.2.9 ile test edilmiştir


3
Bu hacky .. ve çok unnecesairy.
JoeriShoeby

1
Bu nedenle, yukarıdaki tüm diğer çözümler setTimeouts veya gelişmiş Açısal olaylara dayanan geçici çözümlerdir ... bu çözüm tüm ana tarayıcılar tarafından desteklenen saf ES2017 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # arama = bekliyor
JavierFuentes

1
Örneğin Angular + Cordova kullanarak ES2017 özelliklerine güvenemeyeceğiniz bir mobil uygulama (Android API 17'yi hedefleyen) oluşturuyorsanız ne olacak? Kabul edilen cevabın bir çözüm değil, bir çözüm olduğunu unutmayın ..
JoeriShoeby

@JoeriShoeby typescript kullanarak, JS için derlenmiştir, bu nedenle özellik desteği sorunu yoktur.
biggbest

@biggbest Bununla ne demek istiyorsun? Typescript JS derleneceğini biliyorum, ama bu tüm JS çalışacağını varsayamaz. Javascript'in bir web tarayıcısında çalıştırıldığını ve her tarayıcının üzerinde farklı davrandığını unutmayın.
JoeriShoeby

2

Birçok kez kullandığım küçük bir çalışma

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

Ben esas olarak denetleyicide kullandığım değişken ile "null" yerine geçiyorum.


1

Zaten birçok cevap ve değişiklik tespiti ile ilgili çok iyi bir makale bağlantısı olmasına rağmen, iki sentimi burada vermek istedim. Ben kontrol bir nedenle orada olduğunu düşünüyorum, bu yüzden benim app mimarisi hakkında düşündüm ve görünümünde değişiklik BehaviourSubjectve doğru yaşam döngüsü kanca ile ele alınabilir olduğunu fark ettim . İşte bir çözüm için yaptığım şey.

  • Üçüncü taraf bir bileşen ( fullcalendar) kullanıyorum , ancak Açısal Malzeme de kullanıyorum , bu yüzden stil için yeni bir eklenti yapmama rağmen, görünüm ve hissi almak biraz garipti, çünkü repoyu çatallamadan takvim başlığının özelleştirilmesi mümkün değil ve kendiniz yuvarlayın.
  • Bu yüzden temel JavaScript sınıfını aldım ve bileşen için kendi takvim başlığımı başlatmam gerekiyor. Bu, ViewChildebeveynimin işlenmesinden önce oluşturulmasını gerektirir; bu, Açısal'ın çalışma şekli değildir. Bu yüzden şablonum için ihtiyaç duyduğum değeri bir BehaviourSubject<View>(null):

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

Daha sonra, görünümün kontrol edildiğinden emin olabileceğimde, bu konuyu şu değerle güncelliyorum @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);
  }

Sonra, şablonumda sadece asyncboruyu kullanıyorum. Değişiklik tespiti ile hackleme yok, hata yok, sorunsuz çalışıyor.

Lütfen daha fazla bilgiye ihtiyacınız olup olmadığını sormaktan çekinmeyin.


1

Hatayı önlemek için varsayılan form değerini kullanın.

NgAfterViewInit () 'de (benim durumumdaki hatayı da çözdü) detChanges () uygulanmasının kabul edilen yanıtını kullanmak yerine, dinamik olarak gerekli bir form alanı için varsayılan bir değer kaydetmeye karar verdim, böylece form daha sonra güncellendiğinde, kullanıcı formdaki yeni gerekli alanları tetikleyecek (ve gönder düğmesinin devre dışı bırakılmasına neden olacak) bir seçeneği değiştirmeye karar verirse geçerliliği değiştirilmez.

Bu, bileşenime küçük bir kod kaydetti ve benim durumumda hata tamamen önlendi.


Bu gerçekten iyi bir uygulamadır, bununla gereksiz bir çağrıyı önlersinizthis.cdr.detectChanges()
Luis Limas

1

Bu hatayı aldım çünkü bir değişken beyan ettim ve daha sonra
değerini kullanarak değiştirmek istedimngAfterViewInit

export class SomeComponent {

    header: string;

}

geçiş yaptığımı düzeltmek için

ngAfterViewInit() { 

    // change variable value here...
}

için

ngAfterContentInit() {

    // change variable value here...
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.