วิธีจัดการ Angular2“ นิพจน์มีการเปลี่ยนแปลงหลังจากตรวจสอบแล้ว” ยกเว้นเมื่อคุณสมบัติของส่วนประกอบขึ้นอยู่กับวันที่และเวลาปัจจุบัน


168

องค์ประกอบของฉันมีสไตล์ที่ขึ้นอยู่กับวันที่และเวลาปัจจุบัน ในองค์ประกอบของฉันฉันมีฟังก์ชั่นต่อไปนี้

  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คำนวณจาก datetime ปัจจุบัน สีจะเปลี่ยนหากdtoDateอยู่ในช่วง 24 ชั่วโมงที่ผ่านมา

ข้อผิดพลาดที่แน่นอนคือต่อไปนี้:

นิพจน์มีการเปลี่ยนแปลงหลังจากตรวจสอบแล้ว ค่าก่อนหน้า: 'hsl (123, 80%, 49%)' มูลค่าปัจจุบัน: 'hsl (123, 80%, 48%)'

ฉันรู้ว่าข้อยกเว้นปรากฏในโหมดการพัฒนาเท่านั้นในขณะที่ตรวจสอบค่า หากค่าที่เลือกแตกต่างจากค่าที่อัพเดตข้อยกเว้นจะถูกส่งออกไป

ดังนั้นฉันพยายามอัปเดตวันที่และเวลาปัจจุบันในแต่ละรอบการใช้งานในวิธีการขอต่อไปนี้เพื่อป้องกันข้อยกเว้น:

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

... แต่ไม่ประสบความสำเร็จ


คำตอบ:


357

เรียกใช้การตรวจจับการเปลี่ยนแปลงอย่างชัดเจนหลังจากการเปลี่ยนแปลง:

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

โซลูชั่นที่สมบูรณ์แบบขอบคุณ ฉันสังเกตเห็นว่ามันทำงานได้ดีกับเมธอด hook ต่อไปนี้: ngOnChanges, ngDoCheck, ngAfterContentChecked ดังนั้นจะมีตัวเลือกที่ดีที่สุด
Anthony Brenelière

28
ขึ้นอยู่กับกรณีการใช้งานของคุณ หากคุณต้องการทำบางสิ่งบางอย่างเมื่อส่วนประกอบเริ่มต้นngOnInit()มักเป็นที่แรก หากรหัสขึ้นอยู่กับการแสดงผล DOM ngAfterViewInit()หรือngAfterContentInit()เป็นตัวเลือกต่อไป ngOnChanges()เป็นแบบที่ดีถ้ารหัสควรดำเนินการทุกครั้งที่มีการเปลี่ยนแปลงการป้อนข้อมูล ngDoCheck()สำหรับการตรวจจับการเปลี่ยนแปลงที่กำหนดเอง จริงๆแล้วฉันไม่รู้ว่าอะไรngAfterViewChecked()ที่ใช้ดีที่สุด ngAfterViewInit()ฉันคิดว่ามันเรียกว่าก่อนหรือหลัง
GünterZöchbauer

2
@ KushalJayswal ขออภัยไม่สามารถอธิบายได้จากคำอธิบายของคุณ ฉันขอแนะนำให้สร้างคำถามใหม่ด้วยรหัสที่แสดงให้เห็นถึงสิ่งที่คุณพยายามทำให้สำเร็จ เป็นการดีที่มีตัวอย่าง StackBlitz
GünterZöchbauer

4
นี่เป็นวิธีแก้ปัญหาที่ยอดเยี่ยมหากสถานะส่วนประกอบของคุณขึ้นอยู่กับคุณสมบัติ DOM ที่คำนวณโดยเบราว์เซอร์เช่นclientWidthอื่น ๆ
Jonah

1
เพิ่งใช้กับ ngAfterViewInit ทำงานเหมือนมีเสน่ห์
Francisco Arleo

42

TL; DR

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

แม้ว่านี่จะเป็นวิธีแก้ปัญหา แต่บางครั้งก็ยากที่จะแก้ไขปัญหานี้ในแบบที่ดีกว่าดังนั้นอย่าโทษตัวเองถ้าคุณใช้วิธีนี้ ไม่เป็นไร.

ตัวอย่าง : ปัญหาเริ่มต้น [ ลิงก์ ] แก้ไขได้ด้วยsetTimeout()[ ลิงก์ ]


วิธีการหลีกเลี่ยง

โดยทั่วไปข้อผิดพลาดนี้มักจะเกิดขึ้นหลังจากที่คุณเพิ่มที่ไหนสักแห่ง (แม้ในผู้ปกครอง / ngAfterViewInitชิ้นส่วนเด็ก) ดังนั้นคำถามแรกคือถามตัวเองว่าฉันจะอยู่โดยไม่ได้ngAfterViewInitไหม บางทีคุณอาจย้ายรหัสไปที่ไหนสักแห่ง ( ngAfterViewCheckedอาจเป็นทางเลือก)

ตัวอย่าง : [ ลิงค์ ]


ด้วย

นอกจากนี้สิ่ง async ในngAfterViewInitที่มีผลต่อ DOM อาจทำให้เกิดสิ่งนี้ นอกจากนี้ยังสามารถแก้ไขได้ผ่านsetTimeoutหรือโดยการเพิ่มdelay(0)ผู้ประกอบการในท่อ:

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

ตัวอย่าง : [ ลิงค์ ]


อ่านดี

บทความที่ดีเกี่ยวกับวิธีแก้ไขข้อบกพร่องนี้และสาเหตุที่เกิดขึ้น: ลิงก์


2
ดูเหมือนว่าจะช้ากว่าโซลูชันที่เลือก
Pete B

6
นี่ไม่ใช่ทางออกที่ดีที่สุด แต่ใช้งานได้ตลอด คำตอบที่เลือกไม่ได้เสมอไป (หนึ่งในความต้องการที่จะเข้าใจเบ็ดค่อนข้างดีที่จะได้รับมันทำงาน)
เฟร็ดดี้ Bonda

อะไรคือคำอธิบายที่ถูกต้องว่าทำไมงานนี้ทุกคนรู้ เป็นเพราะมันทำงานในเธรดอื่น (async) หรือไม่
knnhcn

3
@knnhcn ไม่มีสิ่งใดเป็นเธรดที่แตกต่างใน javascript JS เป็นเธรดเดียวโดยธรรมชาติ SetTimeout เพียงแค่บอกให้เครื่องยนต์รันฟังก์ชั่นสักพักหลังจากตัวจับเวลาหมดอายุ ที่นี่ตัวจับเวลาคือ 0 ซึ่งถือว่าจริงเป็น 4 ในเบราว์เซอร์ที่ทันสมัยซึ่งมีเวลาเหลือเฟือที่เชิงมุมจะทำสิ่งมหัศจรรย์: developer.mozilla.org/en-US/docs/Web/API/…
Kilves

27

ดังกล่าวโดย @leocaseiro ในปัญหา GitHub

ฉันพบโซลูชัน 3 รายการสำหรับผู้ที่กำลังมองหาวิธีแก้ไขที่ง่าย

1) การย้ายจากngAfterViewInitไปยังngAfterContentInit

2) การย้ายไปngAfterViewCheckedรวมกับChangeDetectorRefที่แนะนำใน # 14748 (ความคิดเห็น)

3) Keep ด้วย ngOnInit () แต่โทรChangeDetectorRef.detectChanges()หลังจากการเปลี่ยนแปลงของคุณ


1
ทุกคนสามารถจัดทำเอกสารโซลูชันที่แนะนำมากที่สุดได้อย่างไร
Pipo

13

เธอไปสองทางแก้ปัญหา!

1. แก้ไขChangeDetectionStrategyเป็น OnPush

สำหรับวิธีนี้คุณจะบอกมุม:

หยุดตรวจสอบการเปลี่ยนแปลง ฉันจะทำก็ต่อเมื่อรู้ว่าจำเป็นเท่านั้น

การแก้ไขอย่างรวดเร็ว:

ปรับเปลี่ยนองค์ประกอบของคุณเพื่อที่จะใช้ ChangeDetectionStrategy.OnPush

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

ด้วยสิ่งนี้ดูเหมือนจะไม่ทำงานอีกต่อไป นั่นเป็นเพราะต่อจากนี้ไปคุณจะต้องทำการโทรdetectChanges()ด้วยตนเองด้วยตนเอง

this.cdr.detectChanges();

นี่คือลิงค์ที่ช่วยให้ฉันเข้าใจChangeDetectionStrategyถูกต้อง: https://alligator.io/angular/change-detection-strategy/

2. การทำความเข้าใจ ExpressionChangedAfterItHasBeenCheckedError

นี่คือสารสกัดขนาดเล็กจากtomonari_t ตอบเกี่ยวกับสาเหตุของข้อผิดพลาดนี้ฉันพยายามรวมเฉพาะส่วนที่ช่วยให้ฉันเข้าใจสิ่งนี้

บทความฉบับเต็มแสดงตัวอย่างรหัสจริงเกี่ยวกับทุกจุดที่แสดงที่นี่

สาเหตุที่แท้จริงคือวงจรชีวิตเชิงมุม:

หลังจากการดำเนินการแต่ละครั้ง Angular จดจำค่าที่ใช้ในการดำเนินการ พวกเขาจะถูกเก็บไว้ในคุณสมบัติ oldValues ​​ของมุมมององค์ประกอบ

หลังจากการตรวจสอบเสร็จสิ้นสำหรับส่วนประกอบทั้งหมดแล้ว Angular จะเริ่มรอบการแยกย่อยถัดไป แต่แทนที่จะทำการดำเนินการจะเปรียบเทียบค่าปัจจุบันกับค่าที่จดจำจากรอบการแยกย่อยก่อนหน้า

การดำเนินการต่อไปนี้เป็นการตรวจสอบที่รอบย่อยของ:

ตรวจสอบว่าค่าที่ส่งผ่านลงไปยังคอมโพเนนต์ชายน์เหมือนกับค่าที่จะใช้เพื่ออัพเดตคุณสมบัติของคอมโพเนนต์เหล่านี้ทันที

ตรวจสอบว่าค่าที่ใช้เพื่ออัปเดตองค์ประกอบ DOM เหมือนกันกับค่าที่จะใช้ในการอัปเดตองค์ประกอบเหล่านี้ในขณะนี้ทำงานเหมือนกัน

ตรวจสอบองค์ประกอบย่อยทั้งหมด

ดังนั้นข้อผิดพลาดจะถูกโยนเมื่อค่าเปรียบเทียบแตกต่างกัน , Blogger Max Koretskyiกล่าวว่า:

ผู้ร้ายมักเป็นส่วนประกอบของลูกหรือคำสั่งเสมอ

และสุดท้ายนี่คือตัวอย่างโลกแห่งความจริงที่มักจะทำให้เกิดข้อผิดพลาดนี้:

  • บริการที่ใช้ร่วมกัน
  • การถ่ายทอดเหตุการณ์แบบซิงโครนัส
  • การสร้างองค์ประกอบแบบไดนามิก

ทุกตัวอย่างสามารถพบได้ที่นี่ (plunkr) ในกรณีของฉันปัญหาคือการสร้างอินสแตนซ์คอมโพเนนต์

นอกจากนี้จากประสบการณ์ของฉันฉันขอแนะนำให้ทุกคนหลีกเลี่ยง setTimeoutแก้ปัญหาในกรณีของฉันทำให้เกิดการวนซ้ำ "เกือบ" (21 สายที่ฉันไม่ต้องการแสดงวิธียั่วพวกเขา)

ฉันขอแนะนำให้คำนึงถึงวงจรชีวิตเชิงมุมของแองกูลาร์เสมอเพื่อให้คุณสามารถพิจารณาว่าจะได้รับผลกระทบได้อย่างไรทุกครั้งที่คุณแก้ไขค่าขององค์ประกอบอื่น ด้วยข้อผิดพลาดนี้ Angular กำลังบอกคุณ:

คุณอาจทำสิ่งนี้ผิดวิธีคุณแน่ใจหรือไม่?

บล็อกเดียวกันก็บอกว่า:

บ่อยครั้งที่การแก้ไขคือการใช้เบ็ดตรวจจับการเปลี่ยนแปลงที่ถูกต้องเพื่อสร้างองค์ประกอบแบบไดนามิก

คำแนะนำสั้น ๆ สำหรับฉันคือการพิจารณาอย่างน้อยสองสิ่งต่อไปนี้ในขณะที่การเขียนโค้ด ( ฉันจะพยายามเติมเต็มเมื่อเวลาผ่านไป ):

  1. หลีกเลี่ยงการแก้ไขค่าองค์ประกอบหลักจากเป็นองค์ประกอบของเด็กแทน: แก้ไขจากองค์ประกอบหลัก
  2. เมื่อคุณใช้@Inputและ@Outputคำสั่งพยายามหลีกเลี่ยงการทริกเกอร์การเปลี่ยนแปลง lyfecycle เว้นแต่ว่าองค์ประกอบจะเริ่มต้นได้อย่างสมบูรณ์
  3. หลีกเลี่ยงการโทรที่ไม่จำเป็นthis.cdr.detectChanges();เพราะอาจทำให้เกิดข้อผิดพลาดมากขึ้นโดยเฉพาะอย่างยิ่งเมื่อคุณต้องจัดการกับข้อมูลแบบไดนามิกจำนวนมาก
  4. เมื่อการใช้งานthis.cdr.detectChanges();เป็นสิ่งจำเป็นตรวจสอบให้แน่ใจว่าตัวแปร ( @Input, @Output, etc) ที่ใช้ถูกเติม / กำหนดค่าเริ่มต้นที่ hook การตรวจสอบที่ถูกต้อง ( OnInit, OnChanges, AfterView, etc)
  5. เมื่อเป็นไปได้ให้ลบออกมากกว่าซ่อนนี่จะเกี่ยวข้องกับจุด 3 และ 4

ด้วย

หากคุณต้องการเข้าใจ Angular Life Hook ฉันขอแนะนำให้คุณอ่านเอกสารอย่างเป็นทางการที่นี่:


ผมก็ยังไม่เข้าใจว่าทำไมเปลี่ยนChangeDetectionStrategyไปOnPushคงมันสำหรับฉัน [disabled]="isLastPage()"ฉันมีองค์ประกอบที่เรียบง่ายซึ่งมี วิธีการที่จะอ่านMatPaginator-ViewChild this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;และกลับมา paginator ไม่สามารถใช้งานได้ทันที แต่หลังจากใช้งาน@ViewChildแล้ว การเปลี่ยนChangeDetectionStrategyข้อผิดพลาดที่ถูกลบ - และการทำงานยังคงมีอยู่เหมือนเดิม ไม่แน่ใจว่าฉันมีข้อบกพร่องอะไรในตอนนี้ แต่ขอบคุณ!
อิกอร์

ยอดเยี่ยม @Igor! การถอนการใช้งานอย่างเดียวOnPushคือคุณจะต้องใช้this.cdr.detectChanges()ทุกครั้งที่คุณต้องการให้องค์ประกอบของคุณรีเฟรช ฉันเดาว่าคุณใช้มันเรียบร้อยแล้ว
Luis Limas

9

ในกรณีของเราเราแก้ไขโดยการเพิ่มการเปลี่ยนแปลงการตรวจสอบเข้าไปในองค์ประกอบและการโทร detectChanges () ใน ngAfterContentChecked รหัสดังนี้

@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

ฉันคิดว่าทางออกที่ดีที่สุดและสะอาดที่สุดที่คุณสามารถจินตนาการได้คือ:

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

ทดสอบกับ Angular 5.2.9


3
นี่คือแฮ็ค .. และไม่น่ารักเลย
JoeriShoeby

1
@JoeriShoeby โซลูชั่นอื่น ๆ ทั้งหมดข้างต้นเป็นวิธีการแก้ปัญหาบนพื้นฐานของ setTimeouts หรือเหตุการณ์เชิงมุมขั้นสูง ... วิธีนี้คือ ES2017 บริสุทธิ์ได้รับการสนับสนุนโดยสาขาวิชาทุกเบราว์เซอร์ developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... caniuse.com / # search = await
JavierFuentes

1
จะเกิดอะไรขึ้นถ้าคุณกำลังสร้างแอปพลิเคชันมือถือ (กำหนดเป้าหมาย Android API 17) โดยใช้ Angular + Cordova ตัวอย่างเช่นคุณไม่สามารถใช้คุณสมบัติ ES2017 ได้ หมายเหตุคำตอบที่ได้รับการยอมรับนั้นเป็นวิธีการแก้ปัญหาไม่ใช่วิธีแก้ปัญหา ..
JoeriShoeby

@JoeriShoeby การใช้ typescript มันถูกคอมไพล์ไปยัง JS ดังนั้นจึงไม่มีปัญหาการสนับสนุนคุณสมบัติ
ใหญ่ที่สุด

@bgbgbest คุณหมายความว่าอย่างไร ฉันรู้ว่า typescript จะถูกรวบรวมไปยัง JS แต่นั่นไม่ได้ทำให้คุณสามารถสมมติว่า JS ทั้งหมดจะทำงานได้ โปรดทราบว่า Javascript นั้นทำงานในเว็บเบราว์เซอร์และทุกเบราว์เซอร์จะทำงานแตกต่างกัน
JoeriShoeby

2

งานเล็ก ๆ รอบ ๆ ฉันใช้หลายครั้ง

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

ฉันแทนที่ "null" ส่วนใหญ่ด้วยตัวแปรบางตัวที่ฉันใช้ในคอนโทรลเลอร์


1

แม้ว่าจะมีคำตอบมากมายอยู่แล้วและลิงก์ไปยังบทความที่ดีมากเกี่ยวกับการตรวจจับการเปลี่ยนแปลง แต่ฉันต้องการให้สองเซ็นต์ของฉันที่นี่ ฉันคิดว่าการตรวจสอบนั้นมีเหตุผลฉันจึงคิดถึงสถาปัตยกรรมของแอพและตระหนักว่าการเปลี่ยนแปลงในมุมมองสามารถจัดการได้โดยการใช้BehaviourSubjectและวงจรชีวิตที่ถูกต้อง ดังนั้นนี่คือสิ่งที่ฉันทำเพื่อแก้ไขปัญหา

  • ฉันใช้องค์ประกอบบุคคลที่สาม ( fullcalendar)แต่ฉันยังใช้Angular Materialดังนั้นแม้ว่าฉันจะสร้างปลั๊กอินใหม่สำหรับการจัดแต่งทรงผมการได้รับรูปลักษณ์และความรู้สึกค่อนข้างอึดอัดเพราะการปรับแต่งส่วนหัวของปฏิทินเป็นไปไม่ได้ และกลิ้งของคุณเอง
  • ดังนั้นฉันจึงได้รับคลาส JavaScript พื้นฐานและต้องเริ่มต้นส่วนหัวปฏิทินของตัวเองสำหรับส่วนประกอบ ที่ต้องViewChildให้มีการเรนเดอร์ก่อนเพราะพ่อแม่ของฉันถูกเรนเดอร์ซึ่งไม่ใช่วิธีการทำงานของแองกูลาร์ นี่คือเหตุผลที่ฉันห่อค่าที่ฉันต้องการสำหรับแม่แบบของฉันในBehaviourSubject<View>(null):

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

ต่อไปเมื่อฉันสามารถตรวจสอบมุมมองได้ฉันอัปเดตหัวเรื่องนั้นด้วยค่าจาก@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);
  }

จากนั้นในเทมเพลตของฉันฉันเพิ่งใช้asyncไปป์ ไม่มีการแฮ็กด้วยการตรวจจับการเปลี่ยนแปลงไม่มีข้อผิดพลาดทำงานได้อย่างราบรื่น

โปรดอย่าลังเลที่จะถามว่าคุณต้องการรายละเอียดเพิ่มเติมหรือไม่


1

ใช้ค่าฟอร์มเริ่มต้นเพื่อหลีกเลี่ยงข้อผิดพลาด

แทนที่จะใช้คำตอบที่ยอมรับของการใช้ detectChanges () ใน ngAfterViewInit () (ซึ่งแก้ไขข้อผิดพลาดในกรณีของฉันด้วย) ฉันตัดสินใจแทนการบันทึกค่าเริ่มต้นสำหรับเขตข้อมูลฟอร์มที่จำเป็นต้องใช้แบบไดนามิกดังนั้นเมื่อปรับปรุงแบบฟอร์มในภายหลัง ความถูกต้องจะไม่เปลี่ยนแปลงหากผู้ใช้ตัดสินใจที่จะเปลี่ยนตัวเลือกในแบบฟอร์มที่จะทริกเกอร์ฟิลด์ที่จำเป็นใหม่ (และทำให้ปุ่มส่งถูกปิดใช้งาน)

สิ่งนี้บันทึกรหัสเล็กน้อยในองค์ประกอบของฉันและในกรณีของฉันข้อผิดพลาดก็หลีกเลี่ยงไปโดยสิ้นเชิง


นี่เป็นวิธีปฏิบัติที่ดีจริงๆโดยคุณหลีกเลี่ยงการโทรไปที่ไม่จำเป็นthis.cdr.detectChanges()
Luis Limas

1

ฉันได้รับข้อผิดพลาดนั้นเพราะฉันประกาศตัวแปรแล้วต้องการ
เปลี่ยนเป็นค่าที่ใช้ในภายหลังngAfterViewInit

export class SomeComponent {

    header: string;

}

เพื่อแก้ไขที่ฉันเปลี่ยนจาก

ngAfterViewInit() { 

    // change variable value here...
}

ถึง

ngAfterContentInit() {

    // change variable value here...
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.