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

168

Почему компонент этого простого 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]

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

Теги:
angular

13 ответов

117
Лучший ответ

Во-первых, обратите внимание, что это исключение будет генерироваться только при запуске вашего приложения в режиме 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 :)')
  }
}
  • 7
    Почему setMessage () не запускает новый раунд обнаружения изменений? Я думал, что Angular 2 автоматически вызывает обнаружение изменений, когда вы изменяете значение чего-либо в пользовательском интерфейсе.
  • 3
    @drewmoore "Все, что изменяет привязку, должно вызывать раунд обнаружения изменений, когда это происходит". Как? Это хорошая практика? Разве все не должно быть завершено за один прогон?
Показать ещё 9 комментариев
191

Как указано в drewmoore, правильным решением в этом случае является ручное инициирование обнаружения изменений для текущего компонента. Это делается с помощью метода detectChanges() объекта ChangeDetectorRef (импортированного из angular2/core) или его метода markForCheck(), который также обновляет любые родительские компоненты. Релевантно пример:

import { Component, ChangeDetectorRef } from 'angular2/core'

@Component({
  selector: 'my-app',
  template: `<div>I'm {{message}} </div>`,
})
export class App {
  message: string = 'loading :(';

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.message = 'all done loading :)'
    this.cdr.detectChanges();
  }

}

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

  • 4
    В моем случае я открывал модал. После открытия модального окна показывалось сообщение «Выражение ___ изменилось после его проверки», поэтому мое решение было добавлено this.cdr.detectChanges (); после открой мой модал. Спасибо!
  • 0
    Где ваша декларация для свойства cdr? Я ожидаю увидеть строку вроде cdr : any или что-то подобное под объявлением message . Просто беспокоишься, что я что-то пропустил?
Показать ещё 3 комментария
32

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

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})
  • 0
    Это сработало для меня. Интересно, в чем разница между этим и использованием ChangeDetectorRef
  • 1
    Хм ... Тогда режим детектора изменений будет изначально установлен на CheckOnce ( документация )
26

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

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

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

ngAfterViewInit() {
  setTimeout(() => {
    this.updateMessage();
  }, 1);
}
  • 3
    Изменение моего кода в метод ngOnInit сработало для меня.
  • 0
    Это должен быть удачный ответ. оно работает.
21

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

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

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

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

6

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

  • 0
    Я использовал комбинацию @ViewChildren() на @ViewChildren() длины @ViewChildren() который был заполнен Observable. Это было единственное решение, которое сработало для меня!
  • 1
    У ngAfterContentChecked сочетание ngAfterContentChecked и ChangeDetectorRef сверху. На ngAfterContentChecked - this.cdr.detectChanges();
4

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

ngOnInit() {
    this.updateMessage();
}

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

1

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

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

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

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

1

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

ngAfterContentInit() {
    this.updateMessage();
}
0

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

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

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

0

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

    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

0

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню