Cách quản lý biểu thức Angular2 đã thay đổi sau khi được kiểm tra ngoại lệ, khi một thuộc tính thành phần phụ thuộc vào thời gian hiện tại


168

Thành phần của tôi có các kiểu phụ thuộc vào datetime hiện tại. Trong thành phần của tôi, tôi đã có chức năng sau đây.

  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được tính từ thời gian hiện tại. Màu sắc thay đổi nếu dtoDatetrong 24 giờ qua.

Lỗi chính xác là như sau:

Biểu hiện đã thay đổi sau khi nó được kiểm tra. Giá trị trước: 'hsl (123, 80%, 49%)'. Giá trị hiện tại: 'hsl (123, 80%, 48%)'

Tôi biết ngoại lệ chỉ xuất hiện trong chế độ phát triển tại thời điểm giá trị được kiểm tra. Nếu giá trị được kiểm tra khác với giá trị được cập nhật, ngoại lệ sẽ bị ném.

Vì vậy, tôi đã cố gắng cập nhật datetime hiện tại ở mỗi vòng đời theo phương thức hook sau để tránh ngoại lệ:

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

...Nhưng không thành công.


Câu trả lời:


357

Chạy phát hiện thay đổi rõ ràng sau khi thay đổi:

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

Giải pháp hoàn hảo, cảm ơn bạn. Tôi nhận thấy nó cũng hoạt động với các phương thức hook sau: ngOnChanges, ngDoCheck, ngAfterContentChecked. Vì vậy, có một lựa chọn tốt nhất?
Anthony Brenelière

27
Điều đó phụ thuộc vào trường hợp sử dụng của bạn. Nếu bạn muốn làm một cái gì đó khi một thành phần được khởi tạo, ngOnInit()thường là nơi đầu tiên. Nếu mã phụ thuộc vào DOM được hiển thị ngAfterViewInit()hoặc ngAfterContentInit()là các tùy chọn tiếp theo. ngOnChanges()là một sự phù hợp tốt nếu mã phải được thực thi mỗi khi thay đổi đầu vào. ngDoCheck()là để phát hiện thay đổi tùy chỉnh. Trên thực tế tôi không biết những gì ngAfterViewChecked()được sử dụng tốt nhất cho. Tôi nghĩ rằng nó được gọi ngay trước hoặc sau ngAfterViewInit().
Günter Zöchbauer

2
@KushalJayswal xin lỗi, không thể hiểu ý nghĩa từ mô tả của bạn. Tôi đề nghị tạo một câu hỏi mới với mã thể hiện những gì bạn cố gắng thực hiện. Lý tưởng với một ví dụ StackBlitz.
Günter Zöchbauer

4
Đây cũng là một giải pháp tuyệt vời nếu trạng thái thành phần của bạn dựa trên các thuộc tính DOM do trình duyệt tính toán, như clientWidth, v.v.
Jonah

1
Chỉ được sử dụng trên ngAfterViewInit, hoạt động như một bùa mê.
Francisco Arleo

42

TL; DR

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

Mặc dù đây là một cách giải quyết, đôi khi thật khó để giải quyết vấn đề này theo bất kỳ cách nào đẹp hơn, vì vậy đừng tự trách mình nếu bạn đang sử dụng phương pháp này. Không sao đâu.

Ví dụ : Vấn đề ban đầu [ liên kết ], Giải quyết bằng setTimeout()[ liên kết ]


Làm sao để tránh

Nói chung, lỗi này thường xảy ra sau khi bạn thêm vào một nơi nào đó (ngay cả trong các thành phần cha / con) ngAfterViewInit. Vì vậy, câu hỏi đầu tiên là tự hỏi mình - tôi có thể sống mà không có ngAfterViewInit? Có lẽ bạn di chuyển mã ở đâu đó ( ngAfterViewCheckedcó thể là một thay thế).

Ví dụ : [ link ]


Cũng thế

Ngoài ra những thứ không đồng bộ trong ngAfterViewInitđó ảnh hưởng đến DOM có thể gây ra điều này. Cũng có thể được giải quyết thông qua setTimeouthoặc bằng cách thêm delay(0)toán tử trong đường ống:

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

Ví dụ : [ link ]


Đọc tốt

Bài viết hay về cách gỡ lỗi này và tại sao nó xảy ra: link


2
Có vẻ chậm hơn so với giải pháp đã chọn
Pete B

6
Đây không phải là giải pháp tốt nhất, nhưng chết tiệt này hoạt động LUÔN. Câu trả lời được chọn không phải lúc nào cũng hoạt động (người ta cần hiểu khá rõ móc để làm cho nó hoạt động)
Freddy Bonda

Giải thích chính xác tại sao điều này hoạt động, bất cứ ai biết? Có phải vì nó sau đó chạy trong một Chủ đề khác (không đồng bộ)?
knnhcn

3
@knnhcn Không có thứ gọi là chủ đề khác nhau trong javascript. JS là đơn luồng theo bản chất. SetTimeout chỉ cho động cơ thực thi chức năng một thời gian sau khi hết giờ. Ở đây, bộ đếm thời gian là 0, thực sự được coi là 4 trong các trình duyệt hiện đại, rất nhiều thời gian để góc cạnh thực hiện những điều kỳ diệu của nó: developer.mozilla.org/en-US/docs/Web/API/
trộm

27

Như @leocaseiro đã đề cập về vấn đề github .

Tôi tìm thấy 3 giải pháp cho những người đang tìm kiếm các bản sửa lỗi dễ dàng.

1) Chuyển từ ngAfterViewInitsangngAfterContentInit

2) Di chuyển đến ngAfterViewCheckedkết hợp với ChangeDetectorRefnhư được đề xuất trên # 14748 (nhận xét)

3) Giữ bằng ngOnInit () nhưng gọi ChangeDetectorRef.detectChanges()sau khi bạn thay đổi.


1
Bất cứ ai có thể tài liệu các giải pháp được đề nghị nhất là gì?
Pipo

13

Cô bạn đi hai giải pháp!

1. Sửa đổi ChangeDetectionStrargety thành OnPush

Đối với giải pháp này, về cơ bản, bạn nói với góc cạnh:

Dừng kiểm tra các thay đổi; tôi sẽ làm điều đó chỉ khi tôi biết là cần thiết

Cách khắc phục nhanh:

Sửa đổi thành phần của bạn để nó sẽ sử dụng ChangeDetectionStrategy.OnPush

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

Với điều này, mọi thứ dường như không còn hoạt động nữa. Đó là bởi vì từ giờ trở đi bạn sẽ phải gọi AngulardetectChanges() theo cách thủ công.

this.cdr.detectChanges();

Đây là một liên kết giúp tôi hiểu đúng ChangeDetectionStrargety : https://alligator.io/angular/change-detection-strargety/

2. Hiểu về ExpressionChangedAfterItHasBeenCheckedError

Đây là một trích xuất nhỏ từ tomonari_t câu trả lời của về nguyên nhân gây ra lỗi này, tôi đã cố gắng chỉ bao gồm các phần giúp tôi hiểu điều này.

Toàn bộ bài viết cho thấy các ví dụ mã thực về mọi điểm được hiển thị ở đây.

Nguyên nhân sâu xa là vòng đời góc cạnh:

Sau mỗi thao tác Angular ghi nhớ những giá trị mà nó đã sử dụng để thực hiện một thao tác. Chúng được lưu trữ trong thuộc tính oldValues ​​của chế độ xem thành phần.

Sau khi kiểm tra xong tất cả các thành phần, Angular sẽ bắt đầu chu trình phân loại tiếp theo nhưng thay vì thực hiện các thao tác, nó so sánh các giá trị hiện tại với các giá trị mà nó nhớ từ chu trình phân loại trước đó.

Các hoạt động sau đây đang được kiểm tra tại chu trình tiêu hóa:

kiểm tra xem các giá trị được truyền xuống các thành phần con có giống với các giá trị sẽ được sử dụng để cập nhật các thuộc tính của các thành phần này không.

kiểm tra xem các giá trị được sử dụng để cập nhật các phần tử DOM có giống với các giá trị sẽ được sử dụng để cập nhật các phần tử này hiện thực hiện như nhau không.

kiểm tra tất cả các thành phần con

Và do đó, lỗi được đưa ra khi các giá trị so sánh là khác nhau. , blogger Max Koretskyi tuyên bố:

Thủ phạm luôn là thành phần con hoặc chỉ thị.

Và cuối cùng đây là một số mẫu trong thế giới thực thường gây ra lỗi này:

  • Chia sẻ dịch vụ
  • Phát sóng sự kiện đồng bộ
  • Khởi tạo thành phần động

Mỗi mẫu có thể được tìm thấy ở đây (plunkr), trong trường hợp của tôi, vấn đề là một khởi tạo thành phần động.

Ngoài ra, bằng kinh nghiệm của riêng tôi, tôi khuyên mọi người nên tránh setTimeoutgiải pháp, trong trường hợp của tôi đã gây ra một vòng lặp vô hạn "gần như" (21 cuộc gọi mà tôi không sẵn sàng chỉ cho bạn cách khiêu khích họ),

Tôi khuyên bạn nên luôn luôn ghi nhớ vòng đời của Angular để bạn có thể tính đến việc chúng sẽ bị ảnh hưởng như thế nào mỗi khi bạn sửa đổi giá trị của thành phần khác. Với lỗi này, Angular đang nói với bạn:

Bạn có thể làm điều này sai cách, bạn có chắc là bạn đúng không?

Blog tương tự cũng nói:

Thông thường, cách khắc phục là sử dụng móc phát hiện thay đổi bên phải để tạo thành phần động

Một hướng dẫn ngắn cho tôi là xem xét ít nhất hai điều sau đây trong khi mã hóa ( tôi sẽ cố gắng bổ sung theo thời gian ):

  1. Thay vào đó, hãy sửa đổi các giá trị thành phần cha mẹ từ các thành phần con của nó: thay đổi chúng từ cha mẹ của nó.
  2. Khi bạn sử dụng @Input@Outputchỉ thị cố gắng tránh kích hoạt thay đổi lyfecycl trừ khi thành phần được khởi tạo hoàn toàn.
  3. Tránh các cuộc gọi không cần thiết của this.cdr.detectChanges(); chúng có thể gây ra nhiều lỗi hơn, đặc biệt khi bạn xử lý nhiều dữ liệu động
  4. Khi việc sử dụng this.cdr.detectChanges();là bắt buộc, hãy đảm bảo rằng các biến ( @Input, @Output, etc) đang được sử dụng được điền / khởi tạo ở móc phát hiện bên phải ( OnInit, OnChanges, AfterView, etc)
  5. Khi có thể, loại bỏ thay vì ẩn , điều này có liên quan đến điểm 3 và 4.

Cũng thế

Nếu bạn muốn hiểu đầy đủ Angular Life Hook, tôi khuyên bạn nên đọc tài liệu chính thức tại đây:


Tôi vẫn không thể hiểu tại sao thay đổi ChangeDetectionStrategyđể OnPushsửa nó cho tôi. Tôi có một thành phần đơn giản, trong đó có [disabled]="isLastPage()". Phương pháp đó là đọc MatPaginator-ViewChild và trả về this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Trình phân trang không có sẵn ngay lập tức, nhưng sau khi nó bị ràng buộc sử dụng @ViewChild. Thay đổi ChangeDetectionStrategylỗi đã xóa - và chức năng vẫn tồn tại như trước đây. Không chắc chắn những nhược điểm bây giờ tôi có, nhưng cảm ơn bạn!
Igor

Thật tuyệt vời @Igor! việc rút tiền sử dụng duy nhất OnPushlà bạn sẽ phải sử dụng this.cdr.detectChanges()mọi lúc bạn muốn làm mới thành phần. Tôi đoán bạn đã sử dụng nó
Luis Limas

9

Trong trường hợp của chúng tôi, chúng tôi CỐ ĐỊNH bằng cách thêm changeDetection vào thành phần và gọi DetChanges () trong ngAfterContentChecked, mã như sau

@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

Tôi nghĩ rằng giải pháp tốt nhất và sạch nhất bạn có thể tưởng tượng là:

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

Đã thử nghiệm với Angular 5.2.9


3
Đây là hacky .. và vì vậy không cần thiết.
JoeriShowise

1
@JoeriVì vậy, tất cả các giải pháp khác ở trên đều là cách giải quyết dựa trên setTimeouts hoặc các sự kiện Angular nâng cao ... giải pháp này hoàn toàn là ES2017 được hỗ trợ bởi tất cả các trình duyệt chuyên ngành developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ tựa caniuse.com / # search = await
JavierFuentes

1
Điều gì xảy ra nếu bạn đang xây dựng một ứng dụng di động (nhắm mục tiêu API Android 17) bằng Angular + Cordova chẳng hạn, nơi bạn không thể dựa vào các tính năng ES2017? Lưu ý câu trả lời được chấp nhận là một giải pháp, không phải là giải pháp ..
JoeriShoither

@JoeriShowise Sử dụng bản thảo, nó được biên dịch thành JS, do đó không có vấn đề hỗ trợ tính năng nào.
biggbest

@biggbest Ý bạn là gì? Tôi biết Typecript sẽ được biên dịch thành JS, nhưng điều đó không làm cho bạn có thể giả định rằng tất cả các JS sẽ hoạt động. Lưu ý rằng Javascript được chạy trong webbrowser và mọi trình duyệt hoạt động khác nhau trên đó.
JoeriShoither 14/11/18

2

Một công việc nhỏ xung quanh tôi đã sử dụng nhiều lần

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

Tôi chủ yếu thay thế "null" bằng một số biến tôi sử dụng trong bộ điều khiển.


1

Mặc dù đã có nhiều câu trả lời và liên kết đến một bài viết rất hay về phát hiện thay đổi, tôi muốn đưa hai xu của mình ở đây. Tôi nghĩ rằng kiểm tra là có lý do vì vậy tôi đã nghĩ về kiến ​​trúc của ứng dụng của mình và nhận ra rằng những thay đổi trong chế độ xem có thể được xử lý bằng cách sử dụng BehaviourSubjectvà móc vòng đời chính xác. Vì vậy, đây là những gì tôi đã làm cho một giải pháp.

  • Tôi sử dụng một thành phần của bên thứ ba ( fullcalWiki) , nhưng tôi cũng sử dụng Angular Material , vì vậy mặc dù tôi đã tạo một plugin mới để tạo kiểu, nhưng giao diện có vẻ hơi khó xử vì không thể tùy chỉnh tiêu đề lịch mà không cần repo và lăn của riêng bạn.
  • Vì vậy, cuối cùng tôi đã nhận được lớp JavaScript cơ bản và cần khởi tạo tiêu đề lịch của riêng tôi cho thành phần. Điều đó đòi hỏi ViewChildphải được kết xuất trước khi bố mẹ tôi được kết xuất, đó không phải là cách Angular hoạt động. Đây là lý do tại sao tôi gói giá trị tôi cần cho mẫu của mình trong BehaviourSubject<View>(null):

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

Tiếp theo, khi tôi có thể chắc chắn chế độ xem được chọn, tôi cập nhật chủ đề đó với giá trị từ @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);
  }

Sau đó, trong mẫu của tôi, tôi chỉ sử dụng asyncđường ống. Không hack với phát hiện thay đổi, không có lỗi, hoạt động trơn tru.

Xin đừng ngần ngại hỏi nếu bạn cần thêm chi tiết.


1

Sử dụng một giá trị biểu mẫu mặc định để tránh lỗi.

Thay vì sử dụng câu trả lời được chấp nhận khi áp dụng DetChanges () trong ngAfterViewInit () (cũng đã giải quyết lỗi trong trường hợp của tôi), tôi đã quyết định thay vào đó là lưu giá trị mặc định cho trường biểu mẫu được yêu cầu động, để khi biểu mẫu được cập nhật sau, tính hợp lệ của nó sẽ không bị thay đổi nếu người dùng quyết định thay đổi một tùy chọn trên biểu mẫu sẽ kích hoạt các trường bắt buộc mới (và khiến nút gửi bị vô hiệu hóa).

Điều này đã lưu một chút mã trong thành phần của tôi và trong trường hợp của tôi, lỗi đã được tránh hoàn toàn.


Đây là một thực hành tốt, với điều này bạn tránh được một cuộc gọi không cần thiết đếnthis.cdr.detectChanges()
Luis Limas

1

Tôi đã gặp lỗi đó vì tôi đã khai báo một biến và sau đó muốn
thay đổi giá trị của nó bằng cách sử dụngngAfterViewInit

export class SomeComponent {

    header: string;

}

để sửa lỗi mà tôi đã chuyển từ

ngAfterViewInit() { 

    // change variable value here...
}

đến

ngAfterContentInit() {

    // change variable value here...
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.