Как управлять исключением «выражение изменилось после проверки» в 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рассчитывается из текущей даты и времени. Цвет меняется, если 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();
}

Идеальное решение, спасибо. Я заметил, что это работает также со следующими методами ловушки: ngOnChanges, ngDoCheck, ngAfterContentChecked. Так есть ли лучший вариант?
Энтони Бренельер,

28
Это зависит от вашего варианта использования. Если вы хотите что-то сделать, когда компонент инициализирован, ngOnInit()обычно это первое место. Если код зависит от визуализируемого DOM ngAfterViewInit()или ngAfterContentInit()есть следующие варианты. ngOnChanges()хорошо подходит, если код должен выполняться каждый раз, когда ввод был изменен. ngDoCheck()для пользовательского обнаружения изменений. На самом деле я не знаю, для чего ngAfterViewChecked()лучше всего использовать. Я думаю, что это называется до или после ngAfterViewInit().
Гюнтер Цохбауэр

2
@KushalJayswal извините, не могу понять из вашего описания. Я бы предложил создать новый вопрос с кодом, который демонстрирует, что вы пытаетесь достичь. Идеально с примером StackBlitz.
Гюнтер Цохбауэр

4
Это также отличное решение, если состояние вашего компонента основано на рассчитанных браузером свойствах DOM, например clientWidth, и т. Д.
Иона

1
Просто используется на ngAfterViewInit, работает как шарм.
Франциско Арлео

42

TL; DR

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

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

Примеры : Первая проблема [ ссылка ], Решена с помощью setTimeout()[ ссылка ]


Как избежать

Как правило, эта ошибка обычно происходит после добавления чего-либо (даже в родительских / дочерних компонентах) ngAfterViewInit. Итак, первый вопрос - спросите себя - могу ли я жить без ngAfterViewInit? Возможно, вы куда-то перемещаете код ( ngAfterViewCheckedможет быть альтернативой).

Пример : [ ссылка ]


Также

Также ngAfterViewInitпричиной этого могут быть асинхронные вещи , влияющие на DOM. Также можно решить с помощью setTimeoutили добавив delay(0)оператор в трубе:

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

Пример : [ ссылка ]


Приятного чтения

Хорошая статья о том, как отладить это и почему это происходит: ссылка


2
Кажется, медленнее, чем выбранное решение
Пит Б

6
Это не лучшее решение, но, черт возьми, это работает ВСЕГДА. Выбранный ответ не всегда работает (нужно хорошо понять хук, чтобы он заработал)
Фредди Бонда

Какое правильное объяснение, почему это работает, кто-нибудь знает? Это потому, что тогда он работает в другом потоке (асинхронном)?
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) Продолжайте с ngOnInit (), но звоните ChangeDetectorRef.detectChanges()после ваших изменений.


1
Кто-нибудь может документировать, что является наиболее рекомендуемым решением?
Пипо

13

Ее вы идете два решения!

1. Измените ChangeDetectionStrategy на OnPush

Для этого решения вы в основном говорите:

Прекратить проверку на изменения; я сделаю это только тогда, когда я знаю, что это необходимо

Быстрое решение:

Измените ваш компонент так, чтобы он использовал ChangeDetectionStrategy.OnPush

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

С этим вещи, кажется, больше не работают. Это потому, что теперь вам придется делать Angular вызов detectChanges()вручную.

this.cdr.detectChanges();

Вот ссылка, которая помогла мне понять ChangeDetectionStrategy правильно: https://alligator.io/angular/change-detection-strategy/

2. Понимание ExpressionChangedAfterItHasBeenCheckedError

Вот небольшой отрывок из ответа tomonari_t о причинах этой ошибки, я попытался включить только те части, которые помогли мне понять это.

Полная статья показывает реальные примеры кода для каждой точки, показанной здесь.

Основной причиной является угловой жизненный цикл:

После каждой операции Angular запоминает, какие значения он использовал для выполнения операции. Они хранятся в свойстве oldValues ​​представления компонента.

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

Следующие операции проверяются в цикле дайджеста:

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

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

проверяет все дочерние компоненты

И так, ошибка выдается, когда сравниваемые значения различны. Блогер Макс Корецкий заявил:

Виновником всегда является дочерний компонент или директива.

И, наконец, вот некоторые примеры из реальной жизни, которые обычно вызывают эту ошибку:

  • Общие услуги
  • Синхронная трансляция событий
  • Создание динамического компонента

Каждый образец можно найти здесь (plunkr), в моем случае проблема заключалась в создании динамического компонента.

Кроме того, по собственному опыту я настоятельно рекомендую всем избегать setTimeoutрешения, в моем случае это вызвало «почти» бесконечный цикл (21 вызов, который я не хочу показывать вам, как их спровоцировать),

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

Возможно, вы делаете это неправильно, вы уверены, что правы?

В том же блоге также говорится:

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

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

  1. Вместо этого избегайте изменения значений родительских компонентов из дочерних компонентов: измените их из родительского.
  2. При использовании @Inputи @Outputдирективы пытаются избежать запуска изменений lyfecycle , если компонент не будет полностью инициализирован.
  3. Избегайте ненужных вызовов this.cdr.detectChanges();, поскольку они могут вызвать больше ошибок, особенно когда вы имеете дело с большим количеством динамических данных
  4. Когда использование this.cdr.detectChanges();является обязательным, убедитесь, что используемые переменные ( @Input, @Output, etc) заполнены / инициализированы в правой точке обнаружения ( 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;. Пагинатор не доступен сразу, но после того, как он был связан с использованием @ViewChild. Изменение ChangeDetectionStrategyубрано, ошибка - и функционал все еще существует, как и прежде. Не уверен, какие недостатки у меня сейчас есть, но спасибо!
Игорь

Это здорово @Igor! единственное ограничение использования OnPush- то, что вам придется использовать this.cdr.detectChanges()каждый раз, когда вы хотите обновить компонент. Я думаю, вы уже использовали его
Луис Лима

9

В нашем случае мы ИСПРАВЛЕНЫ, добавив changeDetection в компонент и вызвав DeteChanges () в 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 или продвинутых событиях Angular ... это решение чисто ES2017, поддерживаемое всеми основными браузерами developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search = await
JavierFuentes

1
Что если вы создаете мобильное приложение (нацеленное на Android API 17), например, на Angular + Cordova, где вы не можете полагаться на функции ES2017? Обратите внимание, что принятый ответ - это решение, а не
обходной путь

@JoeriShoeby Используя машинопись, она скомпилирована в JS, поэтому проблем с поддержкой функций нет.
Biggbest

@biggbest Что ты имеешь в виду под этим? Я знаю, что 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();
});

Я в основном заменяю «ноль» на переменную, которую я использую в контроллере.


1

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

  • Я использую сторонний компонент ( fullcalendar) , но я также использую Angular Material , поэтому, хотя я сделал новый плагин для стилей, получить внешний вид было немного неудобно, потому что настройка заголовка календаря невозможна без разветвления репо и катиться самостоятельно.
  • В итоге я получил базовый класс JavaScript, и мне нужно инициализировать свой собственный заголовок календаря для компонента. Это требует ViewChildотрисовки перед тем, как отчитывается мой родитель, а это не так, как работает Angular. Вот почему я обернул нужное значение для моего шаблона в 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()
Луис Лима

1

Я получил эту ошибку, потому что я объявил переменную и позже хотел
изменить ее значение, используяngAfterViewInit

export class SomeComponent {

    header: string;

}

чтобы исправить это я перешел с

ngAfterViewInit() { 

    // change variable value here...
}

в

ngAfterContentInit() {

    // change variable value here...
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.