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

Почему компонент этого простого plunk

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

метательное:

ИСКЛЮЧЕНИЕ: выражение "Я {{сообщение}} в приложении @0: 5 'изменилось после его проверки. Предыдущее значение:" Я загружаю:( "Текущее значение:" Я все загрузил:)" в [Я {{сообщение}} в приложении @0: 5]

Когда все, что я делаю, обновляет простое связывание при инициировании моего представления?

Ответ 1

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

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

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

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

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

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

ИМХО, более идиоматический способ "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 :)')
  }
}

Ответ 2

Как утверждает 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 на всякий случай.

Ответ 3

Я исправил это, добавив ChangeDetectionStrategy из ядра angular.

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

Ответ 4

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

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

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

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

Ответ 5

ngAfterViewChecked() работал для меня:

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

constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
   //your code to update the model
   this.cdr.detectChanges();
}

Ответ 6

Статья Все, что вам нужно знать о ошибке 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.

Ответ 7

Для этого я попробовал выше ответы, многие из которых не работает в последней версии Angular (6 или более поздняя версия)

Я использую контроль материала, который требует изменений после первого связывания.

    export class AbcClass implements OnInit, AfterContentChecked{
        constructor(private ref: ChangeDetectorRef) {}
        ngOnInit(){
            // your tasks
        }
        ngAfterViewInit() {
            this.ref.detectChanges();
        }
    }

Добавление моего ответа так, это помогает некоторым решить конкретную проблему.

Ответ 8

Вам просто нужно обновить сообщение в правильном ключе жизненного цикла, в данном случае это 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.

Ответ 9

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

ngOnInit() {
    this.updateMessage();
}

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

Ответ 10

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

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

Ответ 11

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

ngAfterContentInit() {
    this.updateMessage();
}

Ответ 12

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

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

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

Ответ 13

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

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

ngAfterViewInit() {
    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 ("Что такое взлом цикла обработки событий?").

Ответ 14

I got similar error while working with datatable. What happens is when you use *ngFor inside another *ngFor datatable throw this error as it interepts angular change cycle. SO instead of using datatable inside datatable use one regular table or replace mf.data with the array name. This works fine.

Ответ 15

Я уже пробовал это решение: (Он работает!)

    export class BComponent {
    name = 'I am B component';
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        setTimeout(() => {
            this.parent.text = 'updated text';
        });
    }

    ngAfterViewInit() {
        setTimeout(() => {
            this.parent.name = 'updated name';
        });
    }
}

Подробнее вы можете прочитать здесь: https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4