简体   繁体   English

Angular 8 上的 ExpressionChangedAfterItHasBeenCheckedError 和在 ngOnInit 上调用的方法

[英]ExpressionChangedAfterItHasBeenCheckedError on Angular 8 and method called on ngOnInit

I'm facing some common issue on Angular template, I have this general template for all my pages and inside of it I have a *ngIf with a spinner inside and another one with my router-outlet.我在 Angular 模板上遇到了一些常见问题,我的所有页面都有这个通用模板,在里面我有一个 *ngIf,里面有一个微调器,另一个有我的路由器插座。

The interceptor controlls the behavior and visibility of the spinner, so with that comes the error, in a specific component I subscribe on a http method on a ngOnInit and the result of that is the error.拦截器控制微调器的行为和可见性,因此在特定组件中,我在 ngOnInit 上订阅了 http 方法,结果就是错误。

This error just happen if I call some method on lifecycle methods.如果我在生命周期方法上调用某些方法,就会发生此错误。 I already tried some workarounds like envolve the setSpinnerState with setTimeout and tried to use other lifecycle hooks(AfterViewInit, AfterContentInit...).我已经尝试了一些解决方法,例如将 setSpinnerState 与 setTimeout 结合,并尝试使用其他生命周期挂钩(AfterViewInit、AfterContentInit ...)。

The interceptor拦截器

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;
  }

}

Layout Service:排版服务:

export class LayoutService {

  private isSpinner: boolean = true;

  constructor() { }

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

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


}

The general template(LayoutComponent)通用模板(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>

The component with the subscribe method带有 subscribe 方法的组件

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);
    });
  }
}

And the error stack和错误堆栈

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)

At last I tried ChangeDetectorRef on LayoutComponent, but error doesn't changed.最后我在 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;
  }

}

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

Original Stackblitz* 原始的 Stackblitz*

ExpressionChangedAfterItHasBeenCheckedError is a development check to ensure bindings update in a manner Angular expects. ExpressionChangedAfterItHasBeenCheckedError是一项开发检查,以确保以 Angular 期望的方式更新绑定。 The order of change detection when it run is as follows:运行时变更检测的顺序如下:

  1. OnInit, DoCheck, OnChanges OnInit、DoCheck、OnChanges
  2. Renders渲染
  3. Change detection runs on child views更改检测在子视图上运行
  4. AfterViewChecked, AfterViewInit AfterViewChecked,AfterViewInit

A final development check makes sure there are no changes after having been checked (else all the children would need to be updated).最终的开发检查确保检查后没有更改(否则所有子项都需要更新)。

The problem is that layoutService.getSpinnerState() is changed by the child after the parent has determined and "rendered" this.问题是layoutService.getSpinnerState()在父级确定并“渲染”后由子级更改。

Stackblitz - A Solution Stackblitz - 一个解决方案

The problem you have boils down to你遇到的问题归结为

  1. Where should you call getUser() ?你应该在哪里调用getUser()

    For the sake of simplicity I've called this in AppComponent .为了简单起见,我在AppComponent中调用了它。

    If you call this in EditProfileComponent OnInit this will trigger an infinite loop.如果你在EditProfileComponent OnInit中调用它,这将触发一个无限循环。

    spinner is false, Parent renders Child, Child calls getUser() , spinner set to true, Parent removes Child and with the http response we repeat. spinner 为 false,Parent 呈现 Child,Child 调用getUser() ,spinner 设置为 true,Parent 删除 Child,并使用 http 响应我们重复。

  2. How to pass data to EditProfileComponent ?如何将数据传递给EditProfileComponent

    For the sake of simplicity I've created a PassDataService to hold the data and then pass this along on EditProfileComponent OnInit .为了简单起见,我创建了一个PassDataService来保存数据,然后在EditProfileComponent OnInit上传递它。

Alternatives include using a guard or resolver or allowing EditProfileComponent to render but having the spinner overlay this until the http response.替代方法包括使用保护或解析器或允许EditProfileComponent渲染但让微调器覆盖它,直到 http 响应。

Unidirectional Data Flow单向数据流

AppComponent → calls getUser() with data saved in service → sets spinnerState true → renders spinner AppComponent → 使用保存在服务中的数据调用 getUser() → 将 spinnerState 设置为 true → 呈现 spinner

On recieving data:在接收数据时:

interceptor → sets spinnerState false → renders router-outlet → renders EditProfileComponent → data passed from service拦截器 → 设置 spinnerState false → 呈现路由器出口 → 呈现 EditProfileComponent → 从服务传递的数据


*This is the same as the original stackblitz but with logging added to give an idea of change detection and the lifecycle hooks as well as putting the template inside the ts file for easier analysis. *这与原始的 stackblitz 相同,但添加了日志记录以提供更改检测和生命周期挂钩的概念,并将模板放入 ts 文件中以便于分析。

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

Seems the spinner state is being set after the value is checked.似乎在检查值后正在设置微调器 state。 Inject NgZone into your InterceptorService and wrap this.layoutService.setSpinnerState within ngZone.NgZone注入到您的InterceptorService中,并将this.layoutService.setSpinnerState包装在 ngZone 中。 This way angular will be aware of the change and no longer complain about it changing.这样 angular 将意识到变化,不再抱怨它的变化。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM