Выражение ___ изменилось после проверки


288

Почему компонент в этом простом панке

@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» изменилось после того, как оно было проверено. Предыдущее значение: «Я загружаю :(». Текущее значение: «Я все загрузил :)» в [I'm {{message}} в приложении @ 0: 5]

когда все, что я делаю, это обновление простой привязки, когда мое представление инициируется?



Подумайте об изменении своего ChangeDetectionStrategyпри использовании detectChanges() stackoverflow.com/questions/39787038/…
Luis Limas

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

Ответы:


295

Как утверждает 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();
  }

}

Вот также Плункеры, демонстрирующие подходы ngOnInit , setTimeout и enableProdMode на всякий случай.


7
В моем случае я открывал модал. После открытия модального окна показывалось сообщение «Выражение ___ изменилось после того, как оно было проверено», поэтому мое решение было добавлено this.cdr.detectChanges (); после открой мой модал. Спасибо!
Хорхе Касариего

Где ваша декларация для свойства cdr? Я бы ожидал увидеть строку вроде cdr : anyили что-то подобное под messageдекларацией. Просто беспокоишься, что я что-то пропустил?
CodeCabbie

1
@CodeCabbie это в аргументах конструктора.
Slasky

1
Это решение исправить мою проблему! Большое спасибо! Очень понятный и простой способ.
Ивазняк

3
Это помогло мне - с несколькими изменениями. Мне пришлось стилизовать элементы li, которые были сгенерированы с помощью цикла ngFor. Мне нужно было изменить цвет промежутков в элементах списка на основе innerText после нажатия кнопки «сортировать», что обновило bool, используемый в качестве параметра канала сортировки (отсортированный результат был копией данных, поэтому стили не получали обновляется только с помощью ngStyle). - Вместо «AfterViewInit» я использовал «AfterViewChecked» - я также обязательно импортировал и реализовал AfterViewChecked. примечание: установка канала 'pure: false' не работала, мне пришлось добавить этот дополнительный шаг (:
Хлоя Корриган

171

Во-первых, обратите внимание, что это исключение будет сгенерировано только при запуске приложения в режиме разработки (что имеет место по умолчанию в бета-0): если вы вызываете enableProdMode()при загрузке приложения, оно не будет выброшено ( см. обновленный план ).

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

В вашем соединении привязка {{message}}изменяется вашим вызовом setMessage(), что происходит в ngAfterViewInitловушке, которая происходит как часть начального хода обнаружения изменений. Это само по себе не является проблемой - проблема в том, что setMessage()изменяет привязку, но не запускает новый раунд обнаружения изменений, а это означает, что это изменение не будет обнаружено, пока какой-то будущий раунд обнаружения изменений не будет запущен где-то еще.

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

Обновите в ответ на все запросы пример того, как это сделать : решение @ Tycho работает, как и три метода в ответе @MarkRajcok. Но, честно говоря, все они мне кажутся уродливыми и неправильными, вроде тех хаков, к которым мы привыкли опираться в ng1.

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

ИМХО, более идиоматический, "Angular2 способ" подхода к этому - что-то вроде: ( plunk )

@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
@ Тихо, действительно. С тех пор как я написал этот комментарий, я ответил на другой вопрос, где я описал 3 способа запуска обнаружения изменений , который включает в себя detectChanges().
Марк Райкок

4
просто чтобы отметить, что в текущем теле вопроса вызванный метод назван updateMessage, а неsetMessage
superjos

3
@Daynil, у меня было такое же чувство, пока я не прочитал блог, приведенный в комментарии под вопросом: blog.angularindepth.com/… Это объясняет, почему это нужно сделать вручную. В этом случае обнаружение изменения угла имеет жизненный цикл. В случае, если что-то меняет значение между этими жизненными циклами, необходимо запустить принудительное обнаружение изменений (или settimeout - который выполняется в следующем цикле событий, снова вызывая обнаружение изменений).
Махеш

51

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
Ari

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

Да, ошибка / предупреждение исчезло. Но это занимает намного больше времени загрузки, чем раньше, как разница 5-7 секунд, которая огромна.
Капил Рагхуванши

1
@KapilRaghuwanshi Бегите this.cdr.detectChanges();за тем, что вы пытаетесь загрузить. Потому что это может быть потому, что обнаружение изменений не срабатывает
Харшал Карпентер

36

Вы не можете использовать, ngOnInitпотому что вы просто меняете переменную-член message?

Если вы хотите получить доступ к ссылке на дочерний компонент @ViewChild(ChildComponent), вам действительно нужно ждать ее ngAfterViewInit.

Грязное исправление - вызвать updateMessage()в следующем цикле событий, например, setTimeout.

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}

4
Изменение моего кода в метод ngOnInit сработало для меня.
Фалиорн

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áč Спасибо за ответ, я обновил свой ответ (y)
MarmiK

1
Я рекомендую этот ответ, если ничего из вышеперечисленного не работает.
Амир Чубани

afterContentChecked вызывается каждый раз, когда DOM пересчитывается или перепроверяется. Intimating from Angular version 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 hook жизненного цикла и добавлю this.cdr.markForCheck (); (cdr для ChangeDetectorRef) внутри него. Разве это не правильный способ проверить изменения после перехвата жизненного цикла и последующей проверки?
Harsimer

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()счетчика, основанного на длине, который был заполнен Observable. Это было единственное решение, которое сработало для меня!
msanford

2
Сочетание ngAfterContentCheckedи ChangeDetectorRefсверху сработало для меня. На ngAfterContentCheckedзвонил -this.cdr.detectChanges();
Кунал Дет

7

Эта ошибка возникает, потому что существующее значение обновляется сразу после инициализации. Поэтому, если вы обновите новое значение после того, как существующее значение будет отображено в DOM, тогда оно будет работать нормально. Как упоминалось в этой статье, Angular Debugging «Выражение изменилось после того, как оно было проверено»

например, вы можете использовать

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, поэтому он будет работать отдельно в стеке браузера и будет ждать завершения элементов стека вызовов.

Как объясняет концепцию API браузера Филип Робертс в одном из видеороликов Youtube («Что такое взлом цикла обработки событий?»).


4

Вы также можете поместить свой вызов updateMessage () в метод ngOnInt (), по крайней мере, он работает для меня

ngOnInit() {
    this.updateMessage();
}

В RC1 это не вызывает исключения


3

Вы также можете создать таймер с помощью Observable.timerфункции rxjs , а затем обновить сообщение в вашей подписке:                    

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

2

Это выдает ошибку, потому что ваш код обновляется при вызове ngAfterViewInit () . Означает, что ваше начальное значение изменилось, когда ngAfterViewInit имеет место. Если вы вызываете это в ngAfterContentInit (), то это не выдаст ошибку.

ngAfterContentInit() {
    this.updateMessage();
}

0

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

Стоит отметить одну вещь! При добавлении changeDetection: ChangeDetectionStrategy.OnPush для компонента не работает, и его дочерний компонент (немой компонент), попробуйте добавить его и к родительскому компоненту.

Это исправило ошибку, но мне интересно, каковы побочные эффекты этого.


0

Я получил аналогичную ошибку при работе с данными. То, что происходит, когда вы используете * ngFor внутри другого * ngFor datatable, генерирует эту ошибку, поскольку она перехватывает цикл угловых изменений. Поэтому вместо использования данных внутри данных используйте одну обычную таблицу или замените mf.data именем массива. Это отлично работает.


0

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

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