简体   繁体   English

Angular ExpressionChangedAfterItHasBeenCheckedError 仅在 ngOnInit() 中

[英]Angular ExpressionChangedAfterItHasBeenCheckedError only in ngOnInit()

I'm trying to add a loginLoading observable where any component can subscribe to it to find weather a user is currently logging in or not.我正在尝试添加一个 loginLoading 可观察对象,任何组件都可以订阅它以查找用户当前是否登录的天气。

In my app.component.html :在我的app.component.html

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

In my app.component.ts :在我的app.component.ts

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

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

In my login.component.ts :在我的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]
        });
}

In my AuthenticationService :在我的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);
            }));
}

Also here is a very simplified example on stackblitz .这里还有一个关于stackblitz的非常简化的例子。

My question is why doesn't angular detect the change in the app's component loginLoading field in this line this.loginLoading = loginLoading;我的问题是为什么 angular 没有检测到这一行中应用程序组件loginLoading字段的变化this.loginLoading = loginLoading; ? Shouldn't this trigger a change detection cycle?这不应该触发变化检测周期吗?

Also if I move the code in the LoginComponent 's ngOnInit() to the LoginComponent 's constructor the error does not appear, does this mean that angular checks for changes after the constructor and befor the ngOnInit() ?此外,如果我将LoginComponentngOnInit()中的代码移动到LoginComponent的构造函数中,则不会出现错误,这是否意味着 angular 检查构造函数之后和ngOnInit()之前的更改?

I solve this by running change detection manually after this line this.loginLoading = loginLoading;我通过在此行之后手动运行更改检测来解决此问题this.loginLoading = loginLoading; in the AppComponent but i'd prefer if i don't or at least know why should I.AppComponent中,但如果我不这样做,或者至少知道我为什么要这样做,我会更愿意。

Edit: I understand that in development mode, angular checks the model didn't change using 1 extra check.编辑:我知道在开发模式下,angular 使用 1 次额外检查检查 model 没有改变。 What I assumed would happen is since an observable firing a new value would trigger a new change detection cycle the error shouldn't appear.我假设会发生的是,因为一个可观察的触发一个新值会触发一个新的变化检测周期,所以错误不应该出现。

To my understanding that if an observable fire between the 2 checks (and it's value is bound to the view), angular wouldn't know (and wouldn't trigger change detection again) and therefore the error appears after the second check据我了解,如果两次检查之间发生可观察到的火灾(并且它的值绑定到视图),angular 将不知道(并且不会再次触发更改检测),因此在第二次检查后出现错误

for the app component use a async pipe for this case, it will help with change detection.对于应用程序组件,在这种情况下使用异步 pipe,它将有助于更改检测。

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

this means you need to store the loading as an observable.这意味着您需要将加载存储为可观察的。 however since you have an issue with expressionChanged, i would instead of having a public value in the Auth service i would just return a new one as where needed.但是,由于您对 expressionChanged 有疑问,我不会在 Auth 服务中拥有公共价值,而是会在需要时返回一个新的价值。

//public loginLoading: Observable<boolean>;

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

this way if timing keeps hitting right between ngOnInit and ngAfterViewInit you can always just set the observable in afterViewInit to avoid the issue.这样,如果计时在 ngOnInit 和 ngAfterViewInit 之间保持正确,您总是可以在 afterViewInit 中设置 observable 来避免这个问题。

app.component.ts

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

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

This happens because you modify Parent state from Child.发生这种情况是因为您修改了 Child 的 Parent state。
Login is child component of app component.登录是应用程序组件的子组件。 With the onInit calling service , the loading flow as below:使用onInit calling service ,加载流程如下:

  1. App Constructor: appComp starts with loading false due to BehaviorSubject => internally, angular save this as oldValue = false App Constructor:由于 BehaviorSubject => 内部,appComp 以加载 false 开始,angular 将其保存为oldValue = false
  2. Login Constructor: None登录构造函数:无
  3. RenderView : aka update binding. RenderView :又名更新绑定。 What is the binding in App? App中的绑定是什么? it's the loading, and the value of it is false .它是加载,它的值为false
    Next is the loginComp binding, no template, we don't care here.接下来是loginComp绑定,没有模板,我们这里不关心。
  4. Now run the lifecycle hooks.现在运行生命周期挂钩。 And because you call the AuthService in onInit.又因为你在onInit中调用了AuthService。 This will run and update the value of loading = true这将运行并更新loading = true的值
  5. We've done now.我们现在已经完成了。 The view is updated in both appComp and LoginComp (AfterViewInit done)视图在 appComp 和 LoginComp 中更新(AfterViewInit 完成)

With Developer mode, Angular will run change detection once again to make sure the view is consistent => Now it come to the appComp, compare the oldValue with the currentVal, which now equals true .在开发人员模式下,Angular 将再次运行更改检测以确保视图一致 => 现在到了 appComp,将oldValue与 currentVal 进行比较,后者现在等于true They are different, which means the view is seeing different values => Wrong, throw Error.它们不同,这意味着视图看到不同的值 => 错误,抛出错误。

What with the constructor?构造函数呢? Why it doesn't throw Error?为什么它不抛出错误? Here is the flow:这是流程:

  1. Same.相同的。 oldValue = false
  2. Login Constructor: Call the authService登录构造器:调用authService
  3. RenderView: This time, as the result of authService, loading now = true, oldValue is updated = true RenderView:这次作为authService的结果,loading now = true, oldValue is updated = true

The rest is the same until Angular run change detection the second time, now the oldValue and the currentValue is the same. rest 是相同的,直到 Angular 第二次运行更改检测,现在oldValuecurrentValue是相同的。 No error.没有错误。

What I've been written here is just a gist of the article below, I just add why constructor doesn't trigger error: https://hackernoon.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4我在这里写的只是下面文章的要点,我只是添加为什么构造函数不会触发错误: https://hackernoon.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror -错误-e3fd9ce7dbb4

The change after checked error is there to stop you from changing things higher up the component tree during change detection, which can lead to the UI not being updated, and therefore being out of sync with the component.检查错误后的更改是为了阻止您在更改检测期间更改组件树更高层的内容,这可能导致 UI 未更新,因此与组件不同步。

Change detection cascades down the tree, and will not backtrack to update UI after it has passed a component.变更检测沿着树向下级联,并且在通过组件后不会回溯以更新 UI。 So having a child modify parent state during change detection de-syncs the UI.因此,让子项在更改检测期间修改父项 state 会使 UI 不同步。 The double check and throwing an error exists in development mode to catch these UI out of sync bugs from happening in production.开发模式中存在双重检查和抛出错误,以捕获生产中发生的这些 UI 不同步错误。 This is just sort of a janky side effect from using a hierarchical change detection system (in my opinion).这只是使用分层变化检测系统(在我看来)的一种不良副作用。

In this case it involves the loading property of your top level component.在这种情况下,它涉及顶级组件的loading属性。 You travel down the component tree to the login component, which indirectly changes the loading property of your top level component during that initial round of change detection.您沿着组件树向下移动到login组件,这会在初始轮次的更改检测期间间接更改顶级组件的loading属性。

You can just push the property change to after change detection runs by pushing the task further down the event loop.您可以通过将任务进一步向下推到事件循环中来将属性更改推到更改检测运行之后。 This can be done using a zero delay setTimeout or queueMicrotask https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#zero_delays .这可以使用零延迟setTimeoutqueueMicrotask https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#zero_delays来完成。

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

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

or you can turn the login function itself into a microtask或者您可以将登录名 function 本身变成微任务

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

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


As for the error not appearing when you put the call in the constructor: the component is instantiated as an object before being attached to the component tree and running change detection / lifecycle hooks, so yes the constructor is run before change detection.至于在构造函数中调用时未出现的错误:组件在附加到组件树并运行更改检测/生命周期挂钩之前被实例化为 object,所以是的,构造函数在更改检测之前运行。 There's nothing wrong with putting the call in the constructor for this purpose.为此将调用放在构造函数中没有错。

The same is true with property initializers, for example this would also avoid the error, but it doesn't make sense for a void return value:属性初始值设定项也是如此,例如这也可以避免错误,但对于 void 返回值没有意义:

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

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


You may also want to just rethink your design, if you are attempting to login immediately, could you just do it in the service rather than a component?您可能还想重新考虑您的设计,如果您尝试立即登录,您可以在服务而不是组件中进行吗?

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

  constructor() {
    this.login();
  }

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

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

this constructor will trigger during the first instance of injecting the service.此构造函数将在注入服务的第一个实例期间触发。 It will be executed before change detection just like component constructors and property initializers.就像组件构造函数和属性初始化器一样,它将在变更检测之前执行。

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

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