Error: ExpressionChangedAfterItHasBeenCheckedError when an Angular Component is tested with synchronous Observables
Error: ExpressionChangedAfterItHasBeenCheckedError when an Angular Component is tested with synchronous Observables
我有一个简单的组件,它使用返回 Observable 的服务。这样的 Observable 用于通过 *ngIf
.
控制 html 元素的创建
代码在这里
@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()
returns 和带有 delay
的 Observable,一切都按预期工作。
现在我删除 delay
并且在控制台上出现以下错误
错误:ExpressionChangedAfterItHasBeenCheckedError:表达式在检查后已更改。以前的值:'ngIf: null'。当前值:'ngIf: [object Object]'.
This is a Stackbliz that reproduces the case.
这是不可避免的吗?有没有办法避免这个错误?
这个小例子复制了一个真实世界的例子,其中 MyService
查询后端,所以延迟。
这对我来说很重要,因为我希望能够 运行 以模拟真实后端的同步方式进行测试,并且当我尝试这种方法时,我得到了相同的结果。
您正在初始化视图后分配您的可观察对象。尝试:
@Input() name: string;
stuff$ = this.myService.getStuff();
或者:
ngOnInit() { // or even in constructor()
this.stuff$ = this.myService.getStuff();
}
当您尝试从 myService 中获取东西时,为什么还要将它作为 @Input 来处理?这是渲染时的一种冲突。
也许 ngAfterViewChecked 比 ngAfterViewInit 效果更好。但解决方案仍然不是很好。
错误基本上是说一个变化触发了另一个变化。它只在没有 delay(100)
的情况下发生,因为 RxJS 是严格顺序的并且订阅是同步发生的。如果您使用 delay(100)
(即使您只使用 delay(0)
,它也会使发射异步,因此它发生在另一个帧中并被另一个变化检测周期捕获。
避免这种情况的一个非常简单的方法就是用 setTimeout()
或 NgZone.run()
.
包装分配给一个变量
或更多 "Rx way" 正在使用 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();
}
有关演示,请参阅 this stackblitz。
我有一个简单的组件,它使用返回 Observable 的服务。这样的 Observable 用于通过 *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()
returns 和带有 delay
的 Observable,一切都按预期工作。
现在我删除 delay
并且在控制台上出现以下错误
错误:ExpressionChangedAfterItHasBeenCheckedError:表达式在检查后已更改。以前的值:'ngIf: null'。当前值:'ngIf: [object Object]'.
This is a Stackbliz that reproduces the case.
这是不可避免的吗?有没有办法避免这个错误?
这个小例子复制了一个真实世界的例子,其中 MyService
查询后端,所以延迟。
这对我来说很重要,因为我希望能够 运行 以模拟真实后端的同步方式进行测试,并且当我尝试这种方法时,我得到了相同的结果。
您正在初始化视图后分配您的可观察对象。尝试:
@Input() name: string;
stuff$ = this.myService.getStuff();
或者:
ngOnInit() { // or even in constructor()
this.stuff$ = this.myService.getStuff();
}
当您尝试从 myService 中获取东西时,为什么还要将它作为 @Input 来处理?这是渲染时的一种冲突。
也许 ngAfterViewChecked 比 ngAfterViewInit 效果更好。但解决方案仍然不是很好。
错误基本上是说一个变化触发了另一个变化。它只在没有 delay(100)
的情况下发生,因为 RxJS 是严格顺序的并且订阅是同步发生的。如果您使用 delay(100)
(即使您只使用 delay(0)
,它也会使发射异步,因此它发生在另一个帧中并被另一个变化检测周期捕获。
避免这种情况的一个非常简单的方法就是用 setTimeout()
或 NgZone.run()
.
或更多 "Rx way" 正在使用 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();
}
有关演示,请参阅 this stackblitz。