繁体   English   中英

Angular ExpressionChangedAfterItHasBeenCheckedError 仅在 ngOnInit() 中

[英]Angular ExpressionChangedAfterItHasBeenCheckedError only in ngOnInit()

我正在尝试添加一个 loginLoading 可观察对象,任何组件都可以订阅它以查找用户当前是否登录的天气。

在我的app.component.html

<mat-toolbar [ngClass]="{'disable-pointer': loginLoading}">
    <a routerLink="login" routerLinkActive="active" id="login"> Login </a>
</mat-toolbar>

在我的app.component.ts

    public loginLoading;
    constructor(private authenticationService: AuthenticationService) {}

    ngOnInit() {
        this.authenticationService.loginLoading.subscribe(loginLoading => {
            this.loginLoading = loginLoading;
        })
    }

在我的login.component.ts中:


constructor(private authenticationService: AuthenticationService){}

ngOnInit(): void {
    this.authenticationService.loginLoading.subscribe(loginLoading => this.loginLoading = loginLoading)
    this.authenticationService.login().subscribe(
        data => {
            this.router.navigate([this.state],);
            console.log(`after:  ${this}`)
        },
        error => {
            this.loginFailed = true
            this.error = error.non_field_errors[0]
        });
}

在我的AuthenticationService中:

private loginLoadingSubject: BehaviorSubject<boolean>;
public loginLoading: Observable<boolean>;


constructor(private http: HttpClient) {
    this.loginLoadingSubject = new BehaviorSubject<boolean>(false);
    this.loginLoading = this.loginLoadingSubject.asObservable();
}


login() {
    this.loginLoadingSubject.next(true)
    return this.http.post<any>(`${environment.apiUrl}/login`, {....})
        .pipe(map(user => {
                .
                .
                this.loginLoadingSubject.next(false)
                .
                .
            }),
            catchError(error => {
                this.loginLoadingSubject.next(false)
                return throwError(error);
            }));
}

这里还有一个关于stackblitz的非常简化的例子。

我的问题是为什么 angular 没有检测到这一行中应用程序组件loginLoading字段的变化this.loginLoading = loginLoading; 这不应该触发变化检测周期吗?

此外,如果我将LoginComponentngOnInit()中的代码移动到LoginComponent的构造函数中,则不会出现错误,这是否意味着 angular 检查构造函数之后和ngOnInit()之前的更改?

我通过在此行之后手动运行更改检测来解决此问题this.loginLoading = loginLoading; AppComponent中,但如果我不这样做,或者至少知道我为什么要这样做,我会更愿意。

编辑:我知道在开发模式下,angular 使用 1 次额外检查检查 model 没有改变。 我假设会发生的是,因为一个可观察的触发一个新值会触发一个新的变化检测周期,所以错误不应该出现。

据我了解,如果两次检查之间发生可观察到的火灾(并且它的值绑定到视图),angular 将不知道(并且不会再次触发更改检测),因此在第二次检查后出现错误

对于应用程序组件,在这种情况下使用异步 pipe,它将有助于更改检测。

<mat-toolbar [ngClass]="{'disable-pointer': loginLoading$ | async}">
    <a routerLink="login" routerLinkActive="active" id="login"> Login </a>
</mat-toolbar>

这意味着您需要将加载存储为可观察的。 但是,由于您对 expressionChanged 有疑问,我不会在 Auth 服务中拥有公共价值,而是会在需要时返回一个新的价值。

//public loginLoading: Observable<boolean>;

getLoginLoading(): Observable<boolean>{
 return this.loginLoadingSubject.asObservable();
}

这样,如果计时在 ngOnInit 和 ngAfterViewInit 之间保持正确,您总是可以在 afterViewInit 中设置 observable 来避免这个问题。

app.component.ts

   public loginLoading$ : observable<boolean>;
    constructor(private authenticationService: AuthenticationService) {}

    ngOnInit() {
        this.loginLoading$ = this.authenticationService.getLoginLoading();
    }

发生这种情况是因为您修改了 Child 的 Parent state。
登录是应用程序组件的子组件。 使用onInit calling service ,加载流程如下:

  1. App Constructor:由于 BehaviorSubject => 内部,appComp 以加载 false 开始,angular 将其保存为oldValue = false
  2. 登录构造函数:无
  3. RenderView :又名更新绑定。 App中的绑定是什么? 它是加载,它的值为false
    接下来是loginComp绑定,没有模板,我们这里不关心。
  4. 现在运行生命周期挂钩。 又因为你在onInit中调用了AuthService。 这将运行并更新loading = true的值
  5. 我们现在已经完成了。 视图在 appComp 和 LoginComp 中更新(AfterViewInit 完成)

在开发人员模式下,Angular 将再次运行更改检测以确保视图一致 => 现在到了 appComp,将oldValue与 currentVal 进行比较,后者现在等于true 它们不同,这意味着视图看到不同的值 => 错误,抛出错误。

构造函数呢? 为什么它不抛出错误? 这是流程:

  1. 相同的。 oldValue = false
  2. 登录构造器:调用authService
  3. RenderView:这次作为authService的结果,loading now = true, oldValue is updated = true

rest 是相同的,直到 Angular 第二次运行更改检测,现在oldValuecurrentValue是相同的。 没有错误。

我在这里写的只是下面文章的要点,我只是添加为什么构造函数不会触发错误: https://hackernoon.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror -错误-e3fd9ce7dbb4

检查错误后的更改是为了阻止您在更改检测期间更改组件树更高层的内容,这可能导致 UI 未更新,因此与组件不同步。

变更检测沿着树向下级联,并且在通过组件后不会回溯以更新 UI。 因此,让子项在更改检测期间修改父项 state 会使 UI 不同步。 开发模式中存在双重检查和抛出错误,以捕获生产中发生的这些 UI 不同步错误。 这只是使用分层变化检测系统(在我看来)的一种不良副作用。

在这种情况下,它涉及顶级组件的loading属性。 您沿着组件树向下移动到login组件,这会在初始轮次的更改检测期间间接更改顶级组件的loading属性。

您可以通过将任务进一步向下推到事件循环中来将属性更改推到更改检测运行之后。 这可以使用零延迟setTimeoutqueueMicrotask https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#zero_delays来完成。

  ngOnInit(): void {
    queueMicrotask(()=>this.authenticationService.login()); // no error
  }

演示: https://stackblitz.com/edit/angular-ivy-wubv1q?file=src/app/login/login.component.ts

或者您可以将登录名 function 本身变成微任务

  login() {
    queueMicrotask(() => {
      this.loginLoadingSubject.next(true);
      setTimeout(() => {
        this.loginLoadingSubject.next(false);
      }, 500);
    });
  }
  ngOnInit(): void {
    this.authenticationService.login() // no error
  }

演示: https://stackblitz.com/edit/angular-ivy-kovyo7?file=src/app/_services/authentication.service.ts


至于在构造函数中调用时未出现的错误:组件在附加到组件树并运行更改检测/生命周期挂钩之前被实例化为 object,所以是的,构造函数在更改检测之前运行。 为此将调用放在构造函数中没有错。

属性初始值设定项也是如此,例如这也可以避免错误,但对于 void 返回值没有意义:

export class LoginComponent {
  _ = this.authenticationService.login(); // no error
  constructor(private authenticationService: AuthenticationService) {}

演示: https://stackblitz.com/edit/angular-ivy-51u7ju?file=src/app/login/login.component.ts


您可能还想重新考虑您的设计,如果您尝试立即登录,您可以在服务而不是组件中进行吗?

export class AuthenticationService {
  private loginLoadingSubject = new BehaviorSubject<boolean>(true);
  public loginLoading = this.loginLoadingSubject.asObservable();

  constructor() {
    this.login();
  }

  login() {
    setTimeout(() => {
      this.loginLoadingSubject.next(false);
    }, 500);
  }
}

演示: https://stackblitz.com/edit/angular-ivy-vumd1c?file=src/app/_services/authentication.service.ts

此构造函数将在注入服务的第一个实例期间触发。 就像组件构造函数和属性初始化器一样,它将在变更检测之前执行。

暂无
暂无

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

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