Вираз ___ змінився після його перевірки


286

Чому компонент у цьому простому планку

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

метання:

ВИХІД: Вираз "Я {{message}} в додатку @ 0: 5" змінився після його перевірки. Попереднє значення: "Я завантажую :(". Поточне значення: "Я все закінчую завантажую :)" в [Я {{message}} в додатку @ 0: 5]

коли все, що я роблю, - це оновлення простої палітурки, коли мій погляд переглядається?



Подумайте про зміну своїх ChangeDetectionStrategyпід час використання detectChanges() stackoverflow.com/questions/39787038/…
Луїс

Подумайте лише про те, чи існує вхідний елемент керування, і ви заповнюєте дані ним методом і тим самим методом ви присвоюєте йому якесь значення. Компілятор точно переплутається з новим / попереднім значенням. Тому зв'язування та заселення має відбуватися різними методами.
MAC

Відповіді:


293

Як зазначає 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
У моєму випадку я відкривав модаль. Після відкриття модалу було показано повідомлення "Вираз ___ змінився після його перевірки", тому до мого рішення було додано this.cdr.detectChanges (); після відкриття моїх модальних. Дякую!
Хорхе Касарієго

Де ваша декларація щодо власності cdr? Я би сподівався побачити такий рядок cdr : anyабо щось подібне під messageдекларацією. Просто хвилююся, що я щось пропустив?
CodeCabbie

1
@CodeCabbie це в аргументах конструктора.
лукавий

1
Це рішення вирішить мою проблему! Дуже дякую! Дуже зрозумілий і простий спосіб.
lwozniak

3
Це мені допомогло - з кількома модифікаціями. Мені довелося стилювати елементи li, які були створені через цикл ngFor. Мені потрібно змінити колір прольотів у елементах списку на основі innerText, натиснувши на кнопку 'Сортувати', яка оновила bool, який використовується як параметр сортування труби (відсортований результат був копією даних, тому стилі не отримували оновлено лише з ngStyle). - Замість використання 'AfterViewInit' я використав 'AfterViewChecked' - я також переконався, що імпортувати та реалізовувати AfterViewChecked. Примітка: налаштування труби на "чисто: помилково" не працює, мені довелося додати цей додатковий крок (:
Хлоя Корріган

170

По-перше, зауважте, що цей виняток буде викинуто лише тоді, коли ви запускаєте додаток у режимі розробки (що за замовчуванням є випадком бета-0). Якщо ви зателефонуєте enableProdMode()під час завантаження програми, він не буде кинутий ( див. оновлений планк ).

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

У вашому планку прив'язка {{message}}змінюється вашим викликом до setMessage(), що відбувається в ngAfterViewInitгачку, що відбувається як частина початкової черги виявлення змін. Це саме по собі не є проблематичним - проблема полягає в тому, що setMessage()змінюється прив'язка, але не викликає новий раунд виявлення змін, що означає, що ця зміна не буде виявлена, поки якийсь майбутній раунд виявлення змін не буде запущений десь в іншому місці.

Винос: все, що змінює прив'язку, потрібно викликати раунд виявлення змін, коли це відбувається.

Оновіть у відповідь на всі запити приклад того, як це зробити : @ рішення Tycho працює, як це вказували три методи у відповіді @MarkRajcok. Але, чесно кажучи, всі вони відчувають мене некрасиво і не так, як той тип хакків, до яких ми звикли спиратися в ng1.

Безумовно, бувають випадкові коли ці хаки доречні, але якщо ви використовуєте їх що-небудь більше, ніж дуже періодично, це знак того, що ви боретеся з рамками, а не повністю сприймаєте її реакційну природу.

ІМХО, більш ідіоматичний, "кутовий шлях2" для підходу до цього - це щось за напрямками: ( планк )

@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 автоматично спрацьовує виявлення змін, коли ви змінюєте значення чогось в інтерфейсі.
Денні Лібін

4
@drewmoore "Все, що змінює прив'язку, повинно викликати цикл виявлення змін, коли це відбувається". Як? Це хороша практика? Чи не повинно все бути виконано за один пробіг?
Данило Біровський Попески

2
@Tycho, справді. Оскільки я написав цей коментар, я відповів на інше запитання, де описую 3 способи запускати виявлення змін , що включає detectChanges().
Марк Райкок

4
просто зауважте, що в поточному органі питання названий метод називається updateMessage, а неsetMessage
superjos

3
@Daynil, у мене було те саме почуття, поки я не прочитав блог, поданий у коментарі під запитанням: blog.angularindepth.com/… Це пояснює, чому це потрібно робити вручну. У цьому випадку виявлення змін кута має життєвий цикл. Якщо щось змінює значення між цими життєвими циклами, тоді необхідно запустити силове виявлення змін (або встановити тайм-аут, який виконується в наступному циклі подій, запускаючи знову виявлення змін).
Махеш

50

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
Ця робота для мене для завантаження динамічного компонента ієрархії всередині разом.
Мехді Даустані

47

Я виправив це, додавши ChangeDetectionStrategy з кутового ядра.

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

Це працювало для мене. Цікаво, в чому різниця між цим та використанням ChangeDetectorRef
Арі

1
Хм ... Тоді спочатку буде встановлено режим детектора змін CheckOnce( документація )
Алекс Клаус

Так, помилка / попередження відсутнє. Але це забирає набагато час завантаження, ніж раніше, як 5-7 секунд різниця, яка величезна.
Kapil Raghuwanshi

1
@KapilRaghuwanshi Запустіть this.cdr.detectChanges();те, що ви намагаєтеся завантажити. Тому що це може бути через те, що виявлення змін не викликає
Харшал Карпентер

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, а не ngAfterViewInit.
Томаш Лукач

Зараз я перебуваю на версії 8.2.0 :)
Томаш Лукач

1
@ TomášLukáč Дякую за відповідь, я оновив свою відповідь (так)
MarmiK

1
Я рекомендую цю відповідь, якщо жодне з перерахованих вище не працює.
Амір Чубані

afterContentChecked викликається кожен раз, коли DOM перераховується або перевіряється. Натхнення від кутової версії 9
Імран Фарукі

24

У статті Все, що потрібно знати про ExpressionChangedAfterItHasBeenCheckedErrorпомилку, пояснює поведінку дуже детально.

Проблема з налаштуванням полягає в тому, що ngAfterViewInitгак життєвого циклу виконується після виявлення змін, оброблених оновленнями DOM. І ви фактично змінюєте властивість, яка використовується в шаблоні в цьому гачку, що означає, що DOM потрібно повторно відредагувати:

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

і для цього знадобиться ще один цикл виявлення змін і Angular за конструкцією виконує лише один цикл дайджесту.

У вас є два варіанти, як це виправити:

  • оновити властивість асинхронно або з використанням setTimeout, Promise.thenабо асинхронні спостерігається посиланням в шаблоні

  • виконати оновлення властивості в гачку перед оновленням DOM - ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked.


Читайте статті: blog.angularindepth.com/… , незабаром прочитайте ще один blog.angularindepth.com/… . Ще не вдалося розібратися у вирішенні цієї проблеми. чи можете ви сказати мені, що станеться, якщо я додам гак життєвого циклу ngDoCheck або ngAfterContentChecked та додаю це this.cdr.markForCheck (); (cdr для ChangeDetectorRef) всередині нього. Це не правильний спосіб перевірити зміни після гачка життєвого циклу та подальшої перевірки.
Гарсімер

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()лічильника на основі довжини, заповненого спостережуваним. Це було єдине рішення, яке працювало на мене!
msanford

2
Поєднання ngAfterContentCheckedі ChangeDetectorRefзгори працювали для мене. На ngAfterContentCheckedназивається -this.cdr.detectChanges();
Кунал 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, наданий браузером, а не API JavaScript, то це буде працювати окремо в стеці браузера і буде чекати, поки елементи стека викликів будуть закінчені.

Філіп Робертс пояснює Філіп Робертс в одному з відео Youtube (Який хак - це цикл подій?).


4

Ви також можете покласти свій дзвінок на updateMessage () в ngOnInt () - метод, принаймні, це працює для мене

ngOnInit() {
    this.updateMessage();
}

У RC1 це не викликає винятку


3

Ви також можете створити таймер за допомогою функції rxjs Observable.timer, а потім оновити повідомлення у своїй підписці:                    

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

2

Це помилка, оскільки ваш код оновлюється, коли ngAfterViewInit () викликається. Призначте, що ваше початкове значення змінилося, коли відбудеться ngAfterViewInit, якщо ви викликаєте це в ngAfterContentInit (), воно не призведе до помилки.

ngAfterContentInit() {
    this.updateMessage();
}

0

Я не міг коментувати пост @Biranchi, оскільки мені не вистачає репутації, але це вирішило проблему для мене.

Одне зауваження! Якщо ви додаєте changeDetection: ChangeDetectionStrategy.OnPush на компоненті не працює, і його дочірній компонент (німий компонент) спробуйте також додати його до батьківського.

Це виправило помилку, але мені цікаво, які побічні ефекти у цього.


0

Я отримав подібну помилку під час роботи з даними. Що відбувається, коли ви використовуєте * ngДля іншого * ngДля даних даних киньте цю помилку, оскільки вона взаємодіє цикл зміни кутових змін. Так замість використання даних, що знаходяться всередині даних, використовуйте одну звичайну таблицю або замініть mf.data на ім'я масиву. Це чудово працює.


0

Я думаю, що найпростішим рішенням було б таке:

  1. Зробіть одну реалізацію, призначивши значення деякій змінній, тобто за допомогою функції або сеттера.
  2. Створіть змінну класу (static working: boolean)в класі, де ця функція існує, і кожного разу, коли ви викликаєте функцію, просто зробіть її справжньою, що вам подобається. У межах функції, якщо значення роботи є істинним, просто повертайтеся відразу, нічого не роблячи. інше, виконайте потрібне завдання. Переконайтеся, що змініть цю змінну на хибну, коли завдання виконано, тобто в кінці рядка кодів або в рамках методу підписки, коли буде зроблено призначення значень!
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.