มีการเปลี่ยนแปลงนิพจน์ ___ หลังจากตรวจสอบแล้ว


287

ทำไมส่วนประกอบในเสียงทุ้มง่าย ๆ นี้

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

ขว้างปา:

ข้อยกเว้น: นิพจน์ 'ฉัน {{ข้อความ}} ในแอป @ 0: 5' มีการเปลี่ยนแปลงหลังจากตรวจสอบแล้ว ค่าก่อนหน้า: 'ฉันกำลังโหลด :(' ค่าปัจจุบัน: 'ฉันโหลดเสร็จแล้ว :)' ใน [ฉัน {{ข้อความ}} ในแอป @ 0: 5]

เมื่อทั้งหมดที่ฉันทำคือการปรับปรุงการผูกง่ายเมื่อเริ่มต้นมุมมองของฉัน?


7
บทความทุกสิ่งที่คุณต้องรู้เกี่ยวกับExpressionChangedAfterItHasBeenCheckedErrorข้อผิดพลาดอธิบายพฤติกรรมในรายละเอียดที่ดี
Max Koretskyi

ลองพิจารณาแก้ไขของคุณChangeDetectionStrategyเมื่อใช้detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas

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

คำตอบ:


294

ตามที่ระบุไว้โดย drewmoore การแก้ปัญหาที่เหมาะสมในกรณีนี้คือการทริกเกอร์การตรวจจับการเปลี่ยนแปลงด้วยตนเองสำหรับองค์ประกอบปัจจุบัน สิ่งนี้ทำโดยใช้detectChanges()วิธีการของChangeDetectorRefวัตถุ (นำเข้าจากangular2/core) หรือmarkForCheck()วิธีการซึ่งทำให้การปรับปรุงองค์ประกอบหลักใด ๆ ตัวอย่างที่เกี่ยวข้อง:

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

}

นี่คือ Plunkers ที่แสดงวิธีngOnInit , setTimeoutและenableProdModeด้วย


7
ในกรณีของฉันฉันกำลังเปิดเป็นกิริยาช่วย หลังจากเปิดเป็นกิริยามันก็แสดงข้อความ "Expression ___ มีการเปลี่ยนแปลงหลังจากที่ถูกตรวจสอบ" ดังนั้นการแก้ปัญหาของฉันถูกเพิ่ม this.cdr.detectChanges (); หลังจากเปิดคำกริยาของฉัน ขอบคุณ!
Jorge Casariego

การประกาศของคุณสำหรับคุณสมบัติ cdr อยู่ที่ไหน ฉันคาดหวังว่าจะเห็นเส้นคล้าย ๆcdr : anyหรืออะไรทำนองนั้นภายใต้การmessageประกาศ แค่กังวลฉันได้พลาดอะไรบางอย่าง?
CodeCabbie

1
@CodeCabbie มันอยู่ในอาร์กิวเมนต์ตัวสร้าง
slasky

1
การแก้ไขนี้แก้ไขปัญหาของฉัน! ขอบคุณมาก! วิธีที่ชัดเจนและเรียบง่ายมาก
lwozniak

3
สิ่งนี้ช่วยฉันได้ - ด้วยการดัดแปลงเล็กน้อย ฉันต้องสไตล์องค์ประกอบ li ที่สร้างขึ้นผ่าน ngFor ห่วง ฉันต้องการเปลี่ยนสีของช่วงภายในรายการตาม innerText เมื่อคลิก 'เรียงลำดับ' ซึ่งอัปเดตบูลที่ใช้เป็นพารามิเตอร์ของไพพ์ไลน์ (ผลลัพธ์เรียงลำดับเป็นสำเนาของข้อมูลดังนั้นสไตล์จึงไม่ได้รับ อัปเดตด้วย ngStyle เพียงอย่างเดียว) - แทนที่จะใช้ 'AfterViewInit' ฉันใช้'AfterViewChecked' - ฉันตรวจสอบให้แน่ใจว่าได้นำเข้าและใช้งาน AfterViewChecked หมายเหตุ: การตั้งค่าไปที่ 'บริสุทธิ์: เท็จ' ไม่ทำงานฉันต้องเพิ่มขั้นตอนพิเศษนี้ (:
Chloe Corrigan

170

ก่อนอื่นโปรดทราบว่าข้อยกเว้นนี้จะถูกโยนทิ้งเมื่อคุณเรียกใช้แอปของคุณในโหมด dev (ซึ่งเป็นกรณีโดยค่าเริ่มต้นเป็น beta-0): หากคุณโทรenableProdMode()เมื่อทำการบู๊ตแอปมันจะไม่ถูกโยน ( ดู อัปเดตเสียงแตก )

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

ในเสียงแตกของคุณการผูก{{message}}จะเปลี่ยนไปตามการเรียกของคุณsetMessage()ซึ่งเกิดขึ้นในngAfterViewInitตะขอซึ่งเกิดขึ้นเป็นส่วนหนึ่งของการตรวจจับการเปลี่ยนแปลงเริ่มต้น ซึ่งในตัวมันเองไม่มีปัญหา - ปัญหาคือsetMessage()การเปลี่ยนแปลงการโยง แต่ไม่ทริกเกอร์การตรวจจับการเปลี่ยนแปลงรอบใหม่หมายความว่าการเปลี่ยนแปลงนี้จะไม่ถูกตรวจพบจนกว่าการตรวจจับการเปลี่ยนแปลงรอบในอนาคตจะถูกเรียกใช้ที่อื่น

Takeaway: สิ่งใดก็ตามที่เปลี่ยนแปลงการโยงจำเป็นต้องกระตุ้นการตรวจจับการเปลี่ยนแปลงรอบเมื่อมันทำ

อัปเดตเพื่อตอบสนองต่อคำขอทั้งหมดสำหรับตัวอย่างของวิธีการดังกล่าว : โซลูชันของ @ Tycho ทำงานได้เช่นเดียวกับสามวิธีในคำตอบ @MarkRajcok แต่ตรงไปตรงมาพวกเขาทุกคนรู้สึกน่าเกลียดและผิดกับฉันเช่นประเภทของแฮ็คที่เราคุ้นเคยในการ ng1

เพื่อให้แน่ใจว่ามีบางครั้งกรณีที่แฮ็กเหล่านี้มีความเหมาะสม แต่ถ้าคุณกำลังใช้พวกเขาในอะไรมากกว่ามากพื้นฐานเป็นครั้งคราวก็เป็นสัญญาณว่าคุณกำลังต่อสู้กรอบมากกว่าอย่างเต็มที่กอดธรรมชาติปฏิกิริยาของมัน

IMHO, สำนวนที่มากขึ้น, "ทาง Angular2" ของการเข้าใกล้นี้เป็นบางสิ่งบางอย่างตามแนวของ: ( เสียงดังปัง )

@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
ทำไม setMessage () ไม่เรียกการตรวจจับการเปลี่ยนแปลงรอบใหม่ ฉันคิดว่า Angular 2 จะทริกเกอร์การตรวจจับการเปลี่ยนแปลงโดยอัตโนมัติเมื่อคุณเปลี่ยนค่าของบางสิ่งใน UI
Danny Libin

4
@drewmoore "สิ่งใดก็ตามที่เปลี่ยนการเชื่อมโยงจำเป็นต้องเรียกใช้รอบการตรวจจับการเปลี่ยนแปลงเมื่อมีการกระทำ" อย่างไร? มันเป็นการปฏิบัติที่ดีหรือไม่? ไม่ควรทุกอย่างเสร็จสมบูรณ์ในการวิ่งครั้งเดียว?
Daniel Birowsky Popeski

2
@Tycho แน่นอน ตั้งแต่ผมเขียนความคิดเห็นที่ฉันได้ตอบคำถามที่ผมอธิบายอีก3 วิธีที่จะเรียกใช้การตรวจจับการเปลี่ยนแปลงdetectChanges()ซึ่งรวมถึง
Mark Rajcok

4
เพียงเพื่อที่จะทราบว่าในร่างกายคำถามปัจจุบันวิธีการเรียกชื่อupdateMessageไม่setMessage
superjos

3
@Daynil ฉันมีความรู้สึกเดียวกันจนกว่าฉันจะอ่านบล็อกที่ให้ไว้ในความคิดเห็นภายใต้คำถาม: blog.angularindepth.com/ ...... มันอธิบายว่าทำไมต้องทำด้วยตนเอง ในกรณีนี้การตรวจจับการเปลี่ยนแปลงของเชิงมุมมีวงจรชีวิต ในกรณีที่บางสิ่งกำลังเปลี่ยนแปลงค่าในระหว่างวงจรชีวิตเหล่านี้การตรวจจับการเปลี่ยนแปลงที่มีพลังจำเป็นต้องถูกเรียกใช้ (หรือ settimeout - ซึ่งดำเนินการในลูปถัดไปของเหตุการณ์เรียกการตรวจจับการเปลี่ยนแปลงอีกครั้ง)
Mahesh

51

ngAfterViewChecked() ทำงานให้ฉัน:

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

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

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

1
การทำงานนี้สำหรับฉันสำหรับการโหลดองค์ประกอบแบบไดนามิกลำดับชั้นอยู่ด้วยกัน
Mehdi Daustany

47

ฉันแก้ไขสิ่งนี้โดยการเพิ่ม ChangeDetectionStrategy จากแกนเชิงมุม

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

สิ่งนี้ใช้ได้สำหรับฉัน ฉันสงสัยว่าอะไรคือความแตกต่างระหว่างสิ่งนี้กับการใช้ ChangeDetectorRef
Ari

1
อืม ... จากนั้นโหมดของเครื่องมือตรวจจับการเปลี่ยนแปลงจะถูกตั้งค่าเริ่มต้นเป็นCheckOnce( เอกสาร )
Alex Klaus

ใช่ข้อผิดพลาด / คำเตือนหายไป แต่มันใช้เวลาโหลดนานกว่าก่อนหน้านี้ประมาณ 5-7 วินาทีซึ่งต่างกันมาก
Kapil Raghuwanshi

1
@KapilRaghuwanshi เรียกใช้this.cdr.detectChanges();หลังจากสิ่งที่คุณพยายามโหลด เพราะอาจเป็นเพราะการตรวจจับการเปลี่ยนแปลงไม่ได้กระตุ้น
Harshal Carpenter

36

คุณไม่สามารถใช้ngOnInitเพราะคุณเพิ่งเปลี่ยนตัวแปรสมาชิกmessageหรือไม่?

หากคุณต้องการที่จะเข้าถึงการอ้างอิงถึงคอมโพเนนต์เด็กคุณแน่นอนจำเป็นต้องรอให้มันด้วย@ViewChild(ChildComponent)ngAfterViewInit

การแก้ไขที่สกปรกคือการเรียกupdateMessage()ในลูปเหตุการณ์ถัดไปด้วยเช่น setTimeout

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

4
การเปลี่ยนรหัสของฉันเป็นวิธี ngOnInit ทำให้ฉันได้
Faliorn

29

สำหรับเรื่องนี้ฉันได้ลองคำตอบข้างต้นแล้วหลายคนไม่สามารถใช้งานได้ใน Angular เวอร์ชันล่าสุด (6 หรือใหม่กว่า)

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

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

การเพิ่มคำตอบของฉันจึงช่วยแก้ปัญหาเฉพาะได้บ้าง


สิ่งนี้ใช้งานได้จริงสำหรับกรณีของฉัน แต่คุณมีข้อผิดพลาดหลังจากใช้ AfterContentChecked คุณควรโทรหา ngAfterContentChecked ไม่ใช่ nAAAViewViewInit
Tomas Lukac

ขณะนี้ฉันใช้เวอร์ชั่น 8.2.0 :)
Tomas Lukac

1
@ TomášLukáčขอบคุณที่ตอบกลับฉันได้อัปเดตคำตอบของฉัน (y)
MarmiK

1
ฉันขอแนะนำคำตอบนี้ถ้าไม่มีข้อใดไม่ได้ผล
Amir Choubani

afterContentChecked ถูกเรียกทุกครั้งที่ DOM ถูกคำนวณใหม่หรือตรวจสอบซ้ำ Intimating จาก Angular version 9
Imran Faruqi

24

บทความทุกสิ่งที่คุณต้องรู้เกี่ยวกับExpressionChangedAfterItHasBeenCheckedErrorข้อผิดพลาดอธิบายพฤติกรรมในรายละเอียดที่ดี

ปัญหาในการตั้งค่าของคุณคือว่าngAfterViewInitlifecycle hook จะถูกดำเนินการหลังจากตรวจพบการเปลี่ยนแปลงการอัพเดต DOM ที่ประมวลผลการตรวจจับ และคุณกำลังเปลี่ยนคุณสมบัติที่ใช้ในเทมเพลตใน hook นี้อย่างมีประสิทธิภาพซึ่งหมายความว่า DOM จะต้องแสดงผลซ้ำ:

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

และสิ่งนี้จะต้องใช้อีกวงจรการตรวจจับการเปลี่ยนแปลงและเชิงมุมโดยการออกแบบเรียกใช้เพียงหนึ่งรอบการย่อย

คุณมีทางเลือกสองทางในการแก้ไข:

  • ปรับปรุงสถานที่ให้บริการแบบไม่พร้อมทั้งการใช้setTimeout, Promise.thenหรือตรงกันสังเกตอ้างอิงในแม่แบบ

  • ดำเนินการอัพเดตคุณสมบัติใน hook ก่อนการอัพเดท DOM - ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked


อ่านบทความของคุณ: blog.angularindepth.com/… , จะอ่านอีกหนึ่งblog.angularindepth.com/…ในไม่ช้า ยังไม่สามารถหาคำตอบสำหรับปัญหานี้ได้ คุณช่วยบอกฉันได้ว่าจะเกิดอะไรขึ้นถ้าฉันเพิ่ม ngDoCheck หรือ ngAfterContentChecked hook lifecycle และเพิ่ม this.cdr.markForCheck (); (cdr สำหรับ ChangeDetectorRef) ข้างใน ไม่ใช่วิธีที่ถูกต้องในการตรวจสอบการเปลี่ยนแปลงหลังจากเบ็ดชีวิตและการตรวจสอบภายหลังเสร็จสมบูรณ์
Harsimer

9

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

เห็น: https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#afterview

ดังนั้นรหัสจะเป็นเพียง:

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

ดูตัวอย่างการทำงานบน Plunker


ฉันใช้ตัวนับ@ViewChildren()ความยาวซึ่งรวมกันโดย Observable นี่เป็นทางออกเดียวที่ได้ผลสำหรับฉัน!
msanford

2
การรวมกันของngAfterContentCheckedและChangeDetectorRefจากข้างต้นทำงานให้ฉัน เมื่อngAfterContentCheckedเรียกว่า - -this.cdr.detectChanges();
Kunal Dethe

7

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

ตัวอย่างเช่นคุณสามารถใช้

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

}

หรือ

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

อย่างที่คุณเห็นฉันยังไม่ได้กล่าวถึงเวลาในเมธอด setTimeout เนื่องจากเป็นเบราว์เซอร์ที่มี API ไม่ใช่ JavaScript API ดังนั้นสิ่งนี้จะทำงานแยกจากกันในเบราว์เซอร์สแต็กและจะรอจนกว่าจะเรียกไอเท็มสแต็กเสร็จสิ้น

Philip Roberts อธิบายแนวคิดของเบราว์เซอร์ API อย่างไรในวิดีโอ Youtube หนึ่งรายการ (แฮ็คคืออีเวนต์ห่วงอะไร)


4

นอกจากนี้คุณยังสามารถโทรเพื่อ updateMessage () ใน ngOnInt () - วิธีการอย่างน้อยมันก็ใช้ได้สำหรับฉัน

ngOnInit() {
    this.updateMessage();
}

ใน RC1 สิ่งนี้ไม่ทำให้เกิดข้อยกเว้น


3

คุณยังสามารถสร้างตัวจับเวลาโดยใช้Observable.timerฟังก์ชั่นrxjs และจากนั้นอัพเดตข้อความในการสมัครสมาชิกของคุณ:                    

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

2

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

ngAfterContentInit() {
    this.updateMessage();
}

0

ฉันไม่สามารถแสดงความคิดเห็นในโพสต์ของ @Biranchi ได้เนื่องจากฉันไม่มีชื่อเสียงเพียงพอ แต่มันแก้ไขปัญหาให้ฉันได้

สิ่งหนึ่งที่ควรทราบ! หากการเพิ่มchangeDetection: ChangeDetectionStrategyOnPushบนองค์ประกอบไม่ทำงานและองค์ประกอบย่อย (องค์ประกอบโง่) ลองเพิ่มไปยังผู้ปกครองด้วย

สิ่งนี้แก้ไขข้อผิดพลาด แต่ฉันสงสัยว่าอะไรคือผลข้างเคียงของสิ่งนี้


0

ฉันพบข้อผิดพลาดคล้ายกันขณะทำงานกับ DataTable สิ่งที่เกิดขึ้นคือเมื่อคุณใช้ * ngFor ภายในอีก * ngFor datatable จะโยนข้อผิดพลาดนี้เนื่องจากมันรบกวนวงจรการเปลี่ยนแปลงเชิงมุม ดังนั้นแทนที่จะใช้ DataTable ภายใน DataTable ให้ใช้หนึ่งตารางปกติหรือแทนที่ mf.data ด้วยชื่ออาร์เรย์ ใช้งานได้ดี


0

ฉันคิดว่าทางออกที่ง่ายที่สุดจะเป็นดังนี้:

  1. ดำเนินการหนึ่งในการกำหนดค่าให้กับตัวแปรบางตัวเช่นผ่านฟังก์ชันหรือตัวตั้งค่า
  2. สร้างตัวแปรคลาส(static working: boolean)ในคลาสที่มีฟังก์ชั่นนี้และทุกครั้งที่คุณเรียกใช้ฟังก์ชันทำให้มันเป็นจริงตามที่คุณต้องการ ภายในฟังก์ชั่นถ้าคุณค่าของการทำงานเป็นจริงจากนั้นก็กลับทันทีโดยไม่ต้องทำอะไร มิฉะนั้นทำงานที่คุณต้องการ ตรวจสอบให้แน่ใจว่าได้เปลี่ยนตัวแปรนี้เป็นเท็จเมื่องานเสร็จสิ้นเช่นตอนท้ายของบรรทัดของรหัสหรือภายในวิธีการสมัครสมาชิกเมื่อคุณกำหนดค่าเสร็จแล้ว!
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.