У меня есть простой компонент, который использует службу, возвращающую Observable. Такой Observable используется для управления созданием html-элемента через *ngIf.
Здесь код
@Component({
selector: 'hello',
template: `<h1 *ngIf = "stuff$ | async as obj">Hello {{obj.name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent implements AfterViewInit {
constructor(
private myService: MyService,
) {}
stuff$;
ngAfterViewInit() {
this.stuff$ = this.myService.getStuff();
}
}
@Injectable({
providedIn: 'root'
})
export class MyService {
getStuff() {
const stuff = {name: 'I am an object'};
return of(stuff).pipe(delay(100));
}
}
Как видите, getStuff() возвращает и Observable с delay, и все работает как положено.
Теперь я удаляю delay и получаю следующую ошибку на консоли
Ошибка: ExpressionChangedAfterItHasBeenCheckedError: выражение изменилось после проверки. Предыдущее значение: «ngIf: null». Текущее значение: «ngIf: [object Object]».
Это Stackbliz, который воспроизводит случай.
Это неизбежно? Есть ли способ избежать этой ошибки?
Этот небольшой пример воспроизводит пример из реальной жизни, где MyService запрашивает серверную часть, поэтому задержка.
Это для меня актуально, потому что я хотел бы иметь возможность синхронно запускать тесты, имитирующие реальный бэкенд, и, когда я пробую такой подход, я получаю тот же результат.





Вы назначаете свой наблюдаемый после, инициализируя представление. Пытаться:
@Input() name: string;
stuff$ = this.myService.getStuff();
Или:
ngOnInit() { // or even in constructor()
this.stuff$ = this.myService.getStuff();
}
Почему вы обрабатываете stuff$ как @Input, когда пытаетесь получить его и из myService? Это своего рода конфликт при рендеринге.
Возможно, ngAfterViewChecked будет работать лучше, чем ngAfterViewInit. Но решение по-прежнему не очень приятно.
Я удалил @Input() name, так как он не актуален.
Ошибка в основном говорит о том, что одно изменение вызвало другое изменение. Это происходит только без delay(100), потому что RxJS строго последователен, а подписки происходят синхронно. Если вы используете delay(100) (даже если вы используете только delay(0), это делает эмиссию асинхронной, поэтому она происходит в другом кадре и перехватывается другим циклом обнаружения изменений.
Очень простой способ избежать этого — просто обернуть присвоение переменной с помощью setTimeout() или с помощью NgZone.run().
Или более «способ Rx» использует оператор subscribeOn(async):
import { asyncScheduler } from 'rxjs';
import { observeOn } from 'rxjs/operators';
...
return of(stuff).pipe(
observeOn(asyncScheduler)
);
Если вам нужно синхронно изменить модель в обработчике жизненного цикла ngAfterViewInit, вы можете активировать обнаружение изменений, чтобы избежать ошибки ExpressionChangedAfterItHasBeenCheckedError:
constructor(private changeDetectorRef: ChangeDetectorRef) { }
ngAfterViewInit() {
this.stuff$ = this.myService.getStuff();
this.changeDetectorRef.detectChanges();
}
См. этот стекблиц для демонстрации.
Я тоже так думал, но посмотри внимательно. Это
@Input() name: string;, а следующая строка —stuff$.