angular - Angular 8 上的 ExpressionChangedAfterItHasBeenCheckedError 和 ngOnInit 上调用的方法
问题描述
我在 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>
带有 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);
});
}
}
和错误堆栈
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
解决方案
原始的 Stackblitz*
ExpressionChangedAfterItHasBeenCheckedError是一种开发检查,以确保以 Angular 期望的方式更新绑定。运行时变更检测的顺序如下:
- OnInit、DoCheck、OnChanges
- 渲染
- 更改检测在子视图上运行
- AfterViewChecked,AfterViewInit
最终的开发检查确保检查后没有更改(否则所有子项都需要更新)。
问题是layoutService.getSpinnerState()
在父母确定并“呈现”这一点后,孩子会改变它。
Stackblitz - 一个解决方案
你遇到的问题归结为
你应该在哪里打电话
getUser()
?为了简单起见,我在AppComponent中调用了它。
如果你在EditProfileComponent OnInit中调用它,这将触发一个无限循环。
spinner 为 false,Parent 呈现 Child,Child 调用getUser(),spinner 设置为 true,Parent 删除 Child,然后我们重复 http 响应。
如何将数据传递给
EditProfileComponent
?为了简单起见,我创建了一个PassDataService来保存数据,然后在EditProfileComponent OnInit上传递它。
替代方案包括使用守卫或解析器或允许EditProfileComponent
渲染但让微调器覆盖它直到 http 响应。
单向数据流
AppComponent → 使用保存在服务中的数据调用 getUser() → 将 spinnerState 设置为 true → 呈现 spinner
在接收数据时:
拦截器 → 设置 spinnerState false → 呈现路由器出口 → 呈现 EditProfileComponent → 从服务传递的数据
*这与原始的 stackblitz 相同,但添加了日志记录以提供更改检测和生命周期挂钩的概念,并将模板放入 ts 文件中以便于分析。
推荐阅读
- javascript - onreadystatechange 事件函数
- java - 删除并重新分配 ClickHandler
- python - django qsstats 中的时间序列问题
- c++ - unique_ptr 中的奇怪段错误
- arrays - 处理加权概率
- vue.js - 重置值时Vuejs计数器条件不起作用
- html - 标题在可重复使用的 HTML 内容块中用于 WYSIWYG 编辑器和 WCAG 有意义的序列
- matlab - Matlab:ButtonDown事件如果在keyPress之后不会触发
- powershell - powershell 重命名脚本 - 主要是功能性的,希望完成并完全自动化任务
- react-native - 在 react-native 导航中更新参数