ExpressionChangedAfterItHasBeenCheckedError Explained

Пожалуйста, объясните мне, почему я продолжаю получать эту ошибку: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

Очевидно, я получаю его только в режиме разработки, этого не происходит в моей производственной сборке, но это очень раздражает, и я просто не понимаю преимуществ ошибки в моей среде разработки, которая не будет отображаться в prod -- вероятно, из-за моего непонимания.

Обычно исправление достаточно простое, я просто обертываю код, вызывающий ошибку, в setTimeout следующим образом:

setTimeout(()=> {
    this.isLoading = true;
}, 0);

Или принудительно обнаружите изменения с помощью конструктора, например: constructor(private cd: ChangeDetectorRef) {}:

this.isLoading = true;
this.cd.detectChanges();

Но почему я постоянно сталкиваюсь с этой ошибкой? Я хочу понять это, чтобы избежать этих хакерских исправлений в будущем.

34 ответа

Решение

Много понимания пришло, когда я понял Angular Lifecycle Hooks и их связь с обнаружением изменений.

Я пытался заставить Angular обновить глобальный флаг, привязанный к *ngIf элемента, и я пытался изменить этот флаг внутри ngOnInit() крюк жизненного цикла другого компонента.

Согласно документации, этот метод вызывается после того, как Angular уже обнаружил изменения:

Вызывается один раз, после первого ngOnChanges().

Таким образом, обновление флага внутри ngOnChanges() не будет инициировать обнаружение изменений. Затем, когда обнаружение изменений снова сработало естественным образом, значение флага изменилось и выдается ошибка.

В моем случае я изменил это:

constructor(private globalEventsService: GlobalEventsService) {

}

ngOnInit() {
    this.globalEventsService.showCheckoutHeader = true;
}

К этому:

constructor(private globalEventsService: GlobalEventsService) {
    this.globalEventsService.showCheckoutHeader = true;
}

ngOnInit() {

}

и это решило проблему:)

У меня была похожая проблема. Глядя на документацию по хукам жизненного цикла, я изменил ngAfterViewInit в ngAfterContentInit и это сработало.

Эта ошибка указывает на реальную проблему в вашем приложении, поэтому имеет смысл выдать исключение.

В devMode обнаружение изменений добавляет дополнительный ход после каждого регулярного запуска обнаружения изменений, чтобы проверить, изменилась ли модель.

Если модель изменилась между обычным и дополнительным поворотом обнаружения изменений, это означает, что либо

  • само обнаружение изменений вызвало изменение
  • метод или метод получения возвращает другое значение каждый раз, когда он вызывается

и то, и другое плохо, потому что не ясно, как действовать, потому что модель может никогда не стабилизироваться.

Если Angular запускает обнаружение изменений до тех пор, пока модель не стабилизируется, он может работать вечно. Если Angular не запускает обнаружение изменений, то представление может не отражать текущее состояние модели.

Смотрите также В чем разница между режимом производства и разработки в Angular2?

Angular запускает обнаружение изменений и когда обнаруживает, что некоторые значения, которые были переданы дочернему компоненту, были изменены, Angular выдает ошибку ExpressionChangedAfterItHasBeenCheckedError, щелкните для More

Чтобы исправить это, мы можем использовать хук жизненного цикла AfterContentChecked и

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

  constructor(
  private cdref: ChangeDetectorRef) { }

  ngAfterContentChecked() {

    this.cdref.detectChanges();

  }

Я использовал ng2-carouselamos (Angular 8 и Bootstrap 4)

Ниже исправлена ​​моя проблема:

Что я сделал:

1. implement AfterViewChecked,  
2. add constructor(private changeDetector : ChangeDetectorRef ) {} and then 
3. ngAfterViewChecked(){ this.changeDetector.detectChanges(); }

Обновить

Я настоятельно рекомендую сначала начать с самостоятельного ответа ОП: правильно подумать о том, что можно сделать в constructor против того, что должно быть сделано в ngOnChanges(),

оригинал

Это скорее примечание, чем ответ, но это может кому-то помочь. Я наткнулся на эту проблему, пытаясь сделать наличие кнопки зависимым от состояния формы:

<button *ngIf="form.pristine">Yo</button>

Насколько я знаю, этот синтаксис приводит к добавлению и удалению кнопки из DOM в зависимости от условия. Что в свою очередь приводит к ExpressionChangedAfterItHasBeenCheckedError,

Исправление в моем случае (хотя я не претендую на полное понимание различий) заключалось в использовании display: none вместо:

<button [style.display]="form.pristine ? 'inline' : 'none'">Yo</button>

Выполните следующие шаги:

1. Используйте "ChangeDetectorRef", импортировав его из @angular/core следующим образом:

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

2. Реализуйте его в конструкторе () следующим образом:

constructor(   private cdRef : ChangeDetectorRef  ) {}

3. Добавьте следующий метод к вашей функции, которую вы вызываете для события, такого как нажатие кнопки. Так это выглядит так:

functionName() {   
    yourCode;  
    //add this line to get rid of the error  
    this.cdRef.detectChanges();     
}

Были интересные ответы, но я, похоже, не нашел того, который соответствовал бы моим потребностям, наиболее близким из @chittrang-mishra, который ссылается только на одну конкретную функцию, а не на несколько переключателей, как в моем приложении.

Я не хотел использовать [hidden] чтобы воспользоваться *ngIf даже не являясь частью DOM, поэтому я нашел следующее решение, которое может быть не лучшим для всех, так как оно подавляет ошибку, а не исправляет ее, но в моем случае, когда я знаю, что конечный результат верен, для моего приложение.

То, что я сделал, было реализовать AfterViewChecked, добавлять constructor(private changeDetector : ChangeDetectorRef ) {} а потом

ngAfterViewChecked(){
  this.changeDetector.detectChanges();
}

Я надеюсь, что это помогает другим, как многие другие помогли мне.

Я столкнулся с той же проблемой, что и значение в одном из массивов моего компонента. Но вместо того, чтобы обнаруживать изменения при изменении значения, я изменил стратегию обнаружения изменения компонента на onPush (который будет обнаруживать изменения при изменении объекта, а не при изменении значения).

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

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush
    selector: -
    ......
})

В моем случае у меня была эта проблема в моем spec-файле при выполнении моих тестов.

Я должен был изменить ngIf в [hidden]

<app-loading *ngIf="isLoading"></app-loading>

в

<app-loading [hidden]="!isLoading"></app-loading>

Ссылаясь на статью https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4

Таким образом, механизм обнаружения изменений фактически работает таким образом, что и дайджесты обнаружения изменений и проверки выполняются синхронно. Это означает, что если мы обновим свойства асинхронно, значения не будут обновлены, когда выполняется цикл проверки, и мы не получим ExpressionChanged... ошибка. Причина, по которой мы получаем эту ошибку, заключается в том, что в процессе проверки Angular видит значения, отличные от тех, которые были записаны на этапе обнаружения изменений. Так что, чтобы избежать этого....

1) Используйте changeDetectorRef

2) использовать setTimeOut. Это выполнит ваш код в другой виртуальной машине как макро-задачу. Angular не увидит эти изменения в процессе проверки, и вы не получите эту ошибку.

 setTimeout(() => {
        this.isLoading = true;
    });

3) Если вы действительно хотите выполнить свой код на той же виртуальной машине, используйте как

Promise.resolve(null).then(() => this.isLoading = true);

Это создаст микро-задачу. Очередь микро-задач обрабатывается после того, как текущий синхронный код завершит выполнение, поэтому обновление свойства произойдет после шага проверки.

Пробовал большинство решений, предложенных выше. Только это сработало для меня в этом сценарии. Я использовал *ngIf для переключения неопределенной прогрессивной полосы углового материала на основе вызовов API, и он бросалExpressionChangedAfterItHasBeenCheckedError.

В рассматриваемом компоненте:

constructor(
    private ngZone: NgZone,
    private changeDetectorRef: ChangeDetectorRef,
) {}

ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
        this.appService.appLoader$.subscribe(value => {
            this.loading = value;
            this.changeDetectorRef.detectChanges();
        });
    });
}

Уловка состоит в том, чтобы обойти обнаружение изменений углового компонента с помощью ngzone.

PS: Не уверен, что это элегантное решение, но использование ловушки жизненного цикла AfterContentChecked и AfterViewChecked обязательно вызовет проблемы с производительностью, поскольку ваше приложение становится больше, поскольку оно запускается много раз.

@HostBinding может быть источником путаницы этой ошибки.

Например, допустим, у вас есть следующая привязка хоста в компоненте

// image-carousel.component.ts
@HostBinding('style.background') 
style_groupBG: string;

Для простоты предположим, что это свойство обновляется с помощью следующего входного свойства:

@Input('carouselConfig')
public set carouselConfig(carouselConfig: string) 
{
    this.style_groupBG = carouselConfig.bgColor;   
}

В родительском компоненте вы программно устанавливаете его в ngAfterViewInit

@ViewChild(ImageCarousel) carousel: ImageCarousel;

ngAfterViewInit()
{
    this.carousel.carouselConfig = { bgColor: 'red' };
}

Вот что происходит:

  • Ваш родительский компонент создан
  • Компонент ImageCarousel создан и назначен carousel (через ViewChild)
  • Мы не можем получить доступ carousel до тех пор ngAfterViewInit() (это будет ноль)
  • Мы назначаем конфигурацию, которая устанавливает style_groupBG = 'red'
  • Это в свою очередь устанавливает background: red на хосте компонента ImageCarousel
  • Этот компонент "принадлежит" вашему родительскому компоненту, поэтому, когда он проверяет изменения, он находит изменение в carousel.style.background и не достаточно умен, чтобы знать, что это не проблема, поэтому он выдает исключение.

Одно из решений состоит в том, чтобы ввести другую оболочку div-инсайдера ImageCarousel и установить для него цвет фона, но тогда вы не получите некоторые преимущества использования HostBinding (например, разрешение родителю контролировать все границы объекта).

Лучшее решение в родительском компоненте состоит в том, чтобы добавить deteChanges() после установки конфигурации.

ngAfterViewInit()
{
    this.carousel.carouselConfig = { ... };
    this.cdr.detectChanges();
}

Это может выглядеть совершенно очевидно, изложенным так, и очень похоже на другие ответы, но есть небольшая разница.

Рассмотрим случай, когда вы не добавляете @HostBinding до поздней стадии разработки. Внезапно вы получаете эту ошибку, и это, кажется, не имеет никакого смысла.

Если вы видите ExpressionChangedAfterItHasBeenCheckedErrorпри срабатывании внутри ngAfterViewInit()тогда вы можете пройти trueпри создании EventEmitterчтобы он излучал асинхронно после текущего цикла обнаружения изменений.

      @Component({
  ...
})
export class MyComponent implements AfterViewInit {
  // Emit asynchronously to avoid ExpressionChangedAfterItHasBeenCheckedError
  @Output() valueChange = new EventEmitter<number>(true);

  ...

  ngAfterViewInit(): void {
    ...
    this.valueChange.emit(newValue);
    ...
  }

}

Вот мои мысли о том, что происходит. Я не читал документацию, но уверен, что это одна из причин, почему ошибка отображается.

*ngIf="isProcessing()" 

При использовании *ngIf он физически меняет DOM, добавляя или удаляя элемент каждый раз, когда изменяется условие. Поэтому, если условие изменяется до того, как оно будет отображено в представлении (что весьма вероятно в мире Angular), возникает ошибка. Смотрите объяснение здесь между режимами разработки и производства.

[hidden]="isProcessing()"

При использовании [скрытый] он не меняет физически DOM, а просто скрывает элемент от вида, скорее всего, с использованием CSS сзади. Элемент все еще находится в DOM, но не виден в зависимости от значения условия. Вот почему ошибка не будет возникать при использовании [скрытый].

Советы по отладке

Эта ошибка может быть довольно запутанной, и легко сделать неверное предположение о том, когда именно она происходит. Я считаю полезным добавить множество отладочных операторов, подобных этому, в соответствующие компоненты в соответствующих местах. Это помогает понять поток.

В родительском операторе put вроде этого (точная строка 'EXPRESSIONCHANGED' важна), но кроме этого это всего лишь примеры:

    console.log('EXPRESSIONCHANGED - HomePageComponent: constructor');
    console.log('EXPRESSIONCHANGED - HomePageComponent: setting config', newConfig);
    console.log('EXPRESSIONCHANGED - HomePageComponent: setting config ok');
    console.log('EXPRESSIONCHANGED - HomePageComponent: running detectchanges');

В обратном вызове ребенка / службы / таймера:

    console.log('EXPRESSIONCHANGED - ChildComponent: setting config');
    console.log('EXPRESSIONCHANGED - ChildComponent: setting config ok');

Если вы бежите detectChanges вручную добавьте запись для этого:

    console.log('EXPRESSIONCHANGED - ChildComponent: running detectchanges');
    this.cdr.detectChanges();

Тогда в отладчике Chrome просто отфильтруйте 'EXPRESSIONCHANGES'. Это покажет вам точно и порядок всего, что установлено, а также точно, в какой момент Angular выдает ошибку.

Вы также можете нажать на серые ссылки, чтобы установить точки останова.

Еще одна вещь, на которую следует обратить внимание, если у вас есть свойства с одинаковыми именами в вашем приложении style.background) убедитесь, что вы отлаживаете тот, который считаете нужным, установив для него непонятное значение цвета.

Моя проблема проявилась, когда я добавил *ngIf но это не было причиной. Ошибка была вызвана изменением модели в {{}} теги, затем пытается отобразить измененную модель в *ngIf заявление позже. Вот пример:

<div>{{changeMyModelValue()}}</div> <!--don't do this!  or you could get error: ExpressionChangedAfterItHasBeenCheckedError-->
....
<div *ngIf="true">{{myModel.value}}</div>

Чтобы исправить проблему, я изменил место, где я вызвал changeMyModelValue(), на место, которое имело больше смысла.

В моей ситуации я хотел, чтобы changeMyModelValue() вызывался всякий раз, когда дочерний компонент изменял данные. Это потребовало, чтобы я создал и отправил событие в дочернем компоненте, чтобы родитель мог обработать его (вызывая changeMyModelValue(). См. https://angular.io/guide/component-interaction

Добавлятьstatic: trueк@ViewChildдекоратор

      @ViewChild(UploadComponent, { static: true }) uploadViewChild: UploadComponent;

Решение, которое работало для меня, используя rxjs

import { startWith, tap, delay } from 'rxjs/operators';

// Data field used to populate on the html
dataSource: any;

....

ngAfterViewInit() {
  this.yourAsyncData.
      .pipe(
          startWith(null),
          delay(0),
          tap((res) => this.dataSource = res)
      ).subscribe();
}

У меня была такая ошибка в Ionic3 (который использует Angular 4 как часть своего технологического стека).

Для меня это было так:

<ion-icon [name]="getFavIconName()"></ion-icon>

Поэтому я пытался условно изменить тип иконки-иона с pin к remove-circleВ режиме, в котором работал экран.

Я предполагаю, что вместо этого мне придется добавить *ngIF.

Для моей проблемы я читал github - "ExpressionChangedAfterItHasBeenCheckedError при изменении значения компонента" не модель "в afterViewInit" и решил добавить ngModel

<input type="hidden" ngModel #clientName />

Это исправило мою проблему, надеюсь, это кому-нибудь поможет.

В моем случае я имел асинхронное свойство в LoadingService с поведенческим предметом isLoading

Использование [скрытой] модели работает, но *ngIf не удается

    <h1 [hidden]="!(loaderService.isLoading | async)">
        THIS WORKS FINE
        (Loading Data)
    </h1>

    <h1 *ngIf="!(loaderService.isLoading | async)">
        THIS THROWS ERROR
        (Loading Data)
    </h1>

Как setTimeout или delay(0) решают эту проблему?

Вот почему приведенный выше код устраняет проблему:

      The initial value of the flag is false, and so the loading indicator will NOT be displayed initially

ngAfterViewInit() gets called, but the data source is not immediately called, so no modifications of the loading indicator will be made synchronously via ngAfterViewInit()

Angular then finishes rendering the view and reflects the latest data changes on the screen, and the Javascript VM turn completes

One moment later, the setTimeout() call (also used inside delay(0)) is triggered, and only then the data source loads its data

the loading flag is set to true, and the loading indicator will now be displayed

Angular finishes rendering the view, and reflects the latest changes on the screen, which causes the loading indicator to get displayed

На этот раз ошибок не возникает, поэтому это исправляет сообщение об ошибке.

источник: https://blog.angular-university.io/angular-debugging/

У меня была эта проблема между RxJS / Observables и статическими фиктивными данными. Сначала в моем приложении использовались статические фиктивные данные, в моем случае - массивы данных.

HTML был таким:

      *ngFor="let x of myArray?.splice(0, 10)"

Таким образом, идея заключалась в отображении до 10 элементов из myArray. splice()берет копию исходного массива. Насколько мне известно, в Angular это прекрасно.

Затем я изменил поток данных на наблюдаемый шаблон, поскольку мои «настоящие» данные поступают из Akita (библиотеки управления состоянием). Это означает, что мой html стал:

      *ngFor="let x of (myArray$ | async)?.splice(0, 10)"

где myArray$ - это [был] тип Observable<MyData[]>, и именно эта манипуляция с данными в шаблоне и стала причиной ошибки. Не делайте этого с объектами RxJS.

Эта ошибка возникает, когда значение изменяется более одного раза в одном и том же цикле обнаружения изменений. У меня была эта проблема с геттером TypeScript, возвращаемое значение которого менялось очень часто. Чтобы исправить это, вы можете ограничить значение так, чтобы оно могло изменяться только один раз за цикл обнаружения изменений следующим образом:

      import { v4 as uuid } from 'uuid'

private changeDetectionUuid: string
private prevChangeDetectionUuid: string
private value: Date

get frequentlyChangingValue(): any {
  if (this.changeDetectionUuid !== this.prevChangeDetectionUuid) {
    this.prevChangeDetectionUuid = this.changeDetectionUuid
    this.value = new Date()
  }
  return this.value
}

ngAfterContentChecked() {
  this.changeDetectionUuid = uuid()
}

HTML:

      <div>{{ frequentlyChangingValue }}</div>

Основной подход здесь заключается в том, что каждый цикл обнаружения изменений имеет свой собственный uuid. Когда uuid изменяется, вы знаете, что находитесь в следующем цикле. Если цикл изменился, обновите значение и верните его, иначе просто верните то же значение, которое было ранее возвращено в этом цикле.

Это гарантирует, что каждый цикл возвращает только одно значение. Это отлично работает для частого обновления значений, учитывая, что циклы обнаружения изменений происходят так часто.

Для создания uuid я использовал модуль uuid npm, но вы можете использовать любой метод, который генерирует уникальный случайный uuid.

Всем, кто борется с этим. Вот способ правильно отладить эту ошибку: https://blog.angular-university.io/angular-debugging/

В моем случае я действительно избавился от этой ошибки с помощью этого [скрытого] хака вместо *ngIf...

Но связь я предоставил позволила мне найти виноватых *ngIf:)

Наслаждаться.

Моя проблема заключалась в том, что я открывал всплывающее окно Ngbmodal при загрузке этого объекта, который изменялся после проверки. Я смог решить эту проблему, открыв модальное всплывающее окно внутри setTimeout.

      setTimeout(() => {
  this.modalReference = this.modalService.open(this.modal, { size: "lg" });
});

Я получил эту ошибку, потому что я использовал переменную в component.html, которая не была объявлена ​​в component.ts. Как только я удалил часть в HTML, эта ошибка исчезла.

Я получил эту ошибку, потому что я отправлял избыточные действия в модальном режиме, и модальный не был открыт в то время. Я отправлял действия в тот момент, когда модальный компонент получал данные. Так что я поместил setTimeout там, чтобы убедиться, что модальный режим открыт, а затем действия сбрасываются.

Я надеюсь, что это поможет кому-то прийти сюда: мы делаем сервисные звонки в ngOnInit следующим образом и использовать переменную displayMain контролировать монтаж элементов на DOM.

component.ts

  displayMain: boolean;
  ngOnInit() {
    this.displayMain = false;
    // Service Calls go here
    // Service Call 1
    // Service Call 2
    // ...
    this.displayMain = true;
  }

и component.html

<div *ngIf="displayMain"> <!-- This is the Root Element -->
 <!-- All the HTML Goes here -->
</div>
Другие вопросы по тегам