【问题标题】:ExpressionChangedAfterItHasBeenCheckedError on Angular 8 and method called on ngOnInitAngular 8 上的 ExpressionChangedAfterItHasBeenCheckedError 和 ngOnInit 上调用的方法
【发布时间】:2020-02-05 03:43:21
【问题描述】:

我在 Angular 模板上遇到了一些常见问题,我的所有页面都有这个通用模板,在其中我有一个 *ngIf,里面有一个微调器,另一个有我的路由器插座。

拦截器控制微调器的行为和可见性,因此错误随之而来,在特定组件中我订阅了 ngOnInit 上的 http 方法,结果就是错误。

如果我在生命周期方法上调用某些方法,就会发生此错误。 我已经尝试了一些解决方法,例如将 setSpinnerState 与 setTimeout 结合,并尝试使用其他生命周期挂钩(AfterViewInit、AfterContentInit...)。

拦截器

export class InterceptorService implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): import('rxjs').Observable<HttpEvent<any>> {
    this.layoutService.setSpinnerState(true);
    return next.handle(this.handlerRequest(req)).pipe(
      delay(3000),
      finalize(() => {
        this.layoutService.setSpinnerState(false);
      })
    );
  }

  constructor(private authService: AuthService, private layoutService: LayoutService) { }

  private handlerRequest(req: HttpRequest<any>) {
    let request;
    if (this.authService.isLoggedIn() && !req.url.includes('/assets/i18n/')) {
      request = req.clone({
        url: environment.api.invokeUrl + req.url,
        headers: req.headers.append('Authorization', 'Bearer ' + this.authService.getAcessToken())
      });
    } else if (req.url.includes('/assets/i18n/')) {
      request = req.clone();
    } else {
      request = req.clone({
        url: environment.api.invokeUrl + req.url
      });
    }
    return request;
  }

}

布局服务:

export class LayoutService {

  private isSpinner: boolean = true;

  constructor() { }

  public setSpinnerState(state: boolean): void {
    this.isSpinner = state;
  }

  public getSpinnerState():boolean{
    return this.isSpinner;
  }


}

通用模板(LayoutComponent)

<div class="page-wrapper fixed-nav-layout">
    <app-header (toggleEvent)="receiveToggle($event)"></app-header>
    <!--Page Body Start-->
    <div class="container-fluid">
        <div class="page-body-wrapper sidebar-icon" [class.sidebar-close]="toggle">
            <app-sidebar class="page-sidebar page-sidebar-open"></app-sidebar>
            <div class="page-body p-t-10">
                <app-breadcrumb></app-breadcrumb>
                <router-outlet  *ngIf="!layoutService.getSpinnerState()"></router-outlet>
                <div class="loader-box d-flex justify-content-center align-self-center" *ngIf="layoutService.getSpinnerState()" async>
                    <div class="loader">
                        <div class="line bg-default"></div>
                        <div class="line bg-default"></div>
                        <div class="line bg-default"></div>
                        <div class="line bg-default"></div>
                    </div>
                </div>
            </div>
        </div>
        <!--Page Body End-->
    </div>

带有订阅方法的组件

export class EditProfileComponent implements OnInit {

  public userProfile = new UserProfile();

  constructor(private userService: UserService) {
  }

  ngOnInit() {
    this.getUser();
  }

  public getUser() {
    // debugger
    this.userService.getUser().subscribe(data => {
      this.userProfile = Amazon.feedUser(data);
    }, err => {
      return new ErrorHandler().show(err.message);
    });
  }
}

还有错误堆栈

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'.
    at viewDebugError (core.js:28792)
    at expressionChangedAfterItHasBeenCheckedError (core.js:28769)
    at checkBindingNoChanges (core.js:29757)
    at checkNoChangesNodeInline (core.js:44442)
    at checkNoChangesNode (core.js:44415)
    at debugCheckNoChangesNode (core.js:45376)
    at debugCheckDirectivesFn (core.js:45273)
    at Object.eval [as updateDirectives] (LayoutsComponent.html:10)
    at Object.debugUpdateDirectives [as updateDirectives] (core.js:45258)
    at checkNoChangesView (core.js:44248)

最后我在 LayoutComponent 上尝试了 ChangeDetectorRef,但错误没有改变。

export class LayoutsComponent implements OnChanges {

  public toggle;
  openToggle: boolean;

  constructor(public navService: NavService, public layoutService: LayoutService, private cd: ChangeDetectorRef) {
    if (this.navService.openToggle === true) {
      this.openToggle = !this.openToggle;
      this.toggle = this.openToggle;
    }
  }

  ngOnChanges(): void {
    this.cd.detectChanges();
  }

  receiveToggle($event) {
    this.openToggle = $event;
    this.toggle = this.openToggle;
  }

}

stackblitz 上重现的错误: https://stackblitz.com/edit/angular-xyulo9

【问题讨论】:

  • 您的问题不清楚,因为 layoutService 缺少且不相关的代码。请在 stackblitz 上复制 mvce,您可以将 http 调用替换为 of(httpData).pipe(delay(100))。我制作了一个demo,但我不确定这与您的示例有多接近。
  • @AndrewAllen 我的 LayoutService 只是将微调器状态设置为 true 或 false。但是我使用该服务编辑了我的问题。我已经尝试过您的解决方案,但错误仍然相同。稍后我将尝试在 stackblitz 上重现该错误。
  • EditProfileComponent 是通过router-outlet 渲染的吗?如果是这样,这应该足以重现错误并提供一些解决方案的想法

标签: angular typescript rxjs


【解决方案1】:

Original Stackblitz*

ExpressionChangedAfterItHasBeenCheckedError 是一种开发检查,以确保绑定以 Angular 期望的方式更新。运行时变化检测的顺序如下:

  1. OnInit、DoCheck、OnChanges
  2. 渲染
  3. 更改检测在子视图上运行
  4. AfterViewChecked、AfterViewInit

最终的开发检查确保在检查后没有更改(否则所有子项都需要更新)。

问题是layoutService.getSpinnerState() 在父级确定并“渲染”它之后被子级更改。

Stackblitz - A Solution

你遇到的问题归结为

  1. 你应该在哪里打电话给getUser()

    为了简单起见,我在 AppComponent 中将其称为。

    如果你在 EditProfileComponent OnInit 中调用它,这将触发一个无限循环。

    spinner 为 false,Parent 渲染 Child,Child 调用 getUser(),spinner 设置为 true,Parent 移除 Child,然后我们重复 http 响应。

  2. 如何将数据传递给EditProfileComponent

    为了简单起见,我创建了一个 PassDataService 来保存数据,然后将其传递给 EditProfileComponent OnInit

替代方法包括使用防护或解析器,或允许 EditProfileComponent 呈现但让微调器覆盖它直到 http 响应。

单向数据流

AppComponent → 使用服务中保存的数据调用 getUser() → 将 spinnerState 设置为 true → 渲染 spinner

在接收数据时:

interceptor → 设置 spinnerState false → 渲染 router-outlet → 渲染 EditProfileComponent → 从服务传递的数据


*这与原始的 stackblitz 相同,但添加了日志记录以提供更改检测和生命周期挂钩的想法,并将模板放入 ts 文件中以便于分析。

【讨论】:

    【解决方案2】:
    finalize(() => {
      this.layoutService.setSpinnerState(false);
    })
    

    似乎在检查值后正在设置微调器状态。将NgZone 注入您的InterceptorService 并将this.layoutService.setSpinnerState 包装在ngZone 中。这样 Angular 就会意识到变化,不再抱怨它的变化。

    this.ngZone.run(() => {
      this.layoutService.setSpinnerState(false);
    });
    

    【讨论】:

    • 所以 Noremac,我尝试了您的解决方案,但拦截器不接受返回 void 的函数并抛出错误:``` 类型为 'void' 的参数不可分配给类型为 'OperatorFunction 的参数>' ``` 我尝试用 finalize 进行包装,但也不起作用。 ````
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-30
    • 2022-11-22
    • 2019-07-03
    • 2020-04-03
    • 2019-07-24
    • 1970-01-01
    • 2021-05-29
    相关资源
    最近更新 更多