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