Як керувати виключенням виразу 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. Тож чи є найкращий варіант?
Anthony Brenelière

28
Це залежить від вашого випадку використання. Якщо ви хочете щось зробити, коли компонент ініціалізується, ngOnInit()зазвичай це перше місце. Якщо код залежить від виводу DOM ngAfterViewInit()або ngAfterContentInit()є наступними параметрами. ngOnChanges()добре підходить, якщо код слід виконувати щоразу, коли зміна введення. ngDoCheck()призначений для виявлення користувацьких змін. Насправді я не знаю, для чого ngAfterViewChecked()найкраще використовувати. Я думаю, це називається безпосередньо перед чи після ngAfterViewInit().
Günter Zöchbauer

2
@KushalJayswal вибачте, не може мати сенсу з вашого опису. Я б запропонував створити нове запитання з кодом, який демонструє те, що ви намагаєтеся виконати. В ідеалі на прикладі StackBlitz.
Günter Zöchbauer

4
Це також відмінне рішення, якщо стан вашого компонента базується на обчислюваних у браузері властивостях DOM, наприклад clientWidth, тощо.
Jonah

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
Чи може хтось документувати, що є найбільш рекомендованим рішенням?
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, такі самі, як і значення, які використовувались для оновлення цих елементів, тепер виконують ті самі.

перевірки на всі дочірні компоненти

І так, помилка кидається, коли порівняні значення відрізняються. , блогер Макс Корецький заявив:

Винуватець завжди є дочірнім компонентом чи директивою.

І, нарешті, ось кілька зразків реального світу, які зазвичай викликають цю помилку:

  • Спільні послуги
  • Синхронне мовлення подій
  • Динамічна інстанція компонентів

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

Крім того, на власному досвіді я настійно рекомендую всім уникати setTimeoutрішення, в моєму випадку спричинив "майже" нескінченну петлю (21 дзвінок, який я не бажаю показати вам, як їх спровокувати),

Я рекомендую завжди пам’ятати про кутовий життєвий цикл, щоб ви могли врахувати, як на них впливати кожен раз, коли ви змінюватимете значення іншого компонента. З цією помилкою Angular каже вам:

Ви, можливо, робите це неправильно, ви впевнені, що маєте рацію?

У цьому ж блозі йдеться також:

Часто виправленням є використання правильного гака виявлення змін для створення динамічного компонента

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

  1. Не змінюйте значення батьківських компонентів із компонентів його дочірніх даних, замість цього: модифікуйте їх з його батьківського.
  2. При використанні @Inputі@Output директив намагайтеся уникати спроб зміни ліфтоциклу, якщо компонент не буде повністю ініціалізований.
  3. Уникайте зайвих дзвінків this.cdr.detectChanges(); вони можуть викликати більше помилок, особливо коли ви маєте справу з великою кількістю динамічних даних
  4. Коли використання this.cdr.detectChanges();обов'язкове, переконайтеся, що використовувані змінні ( @Input, @Output, etc) заповнюються / ініціалізуються на правій гачці виявлення ( OnInit, OnChanges, AfterView, etc)
  5. Коли це можливо, видаліть, а не прихойте , це пов’язано з пунктами 3 та 4.

Також

Якщо ви хочете повністю зрозуміти кутовий рятувальний гачок, рекомендую ознайомитись з офіційною документацією тут:


Я до сих пір не можу зрозуміти, чому змінив 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 в компонент і виклик 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 }
    );
  }
}

Перевірено з кутовим 5.2.9


3
Це хакі .. і так непотрібно.
JoeriShoeby

1
@JoeriShoeby всі інші рішення, описані вище, є обхідними методами на основі setTimeouts або розширених подій Angular ... це рішення є чистим ES2017, підтримуваним усіма основними браузерами developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # пошук =
чекай

1
Що робити, якщо ви створюєте мобільний додаток (орієнтований на Android API 17), використовуючи, наприклад, Angular + Cordova, коли ви не можете покластися на функції ES2017? Зауважте, що прийнята відповідь - це рішення, а не обхід ..
JoeriShoeby

@JoeriShoeby Використовуючи typecript, він компілюється в 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();
});

Я в основному замінюю "null" на деяку змінну, яку використовую в контролері.


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.