Angular 4中MatSnackbar触发ExpressionChangedAfterItHasBeenCheckedError的解决方法
我太懂这个问题了——Angular的变更检测机制有时候就是会在这种场景下“挑刺”。咱们先搞清楚为啥会报错:
当你的认证守卫把用户重定向到登录页时,登录页的ngOnInit会在Angular的初始变更检测周期内触发snackbar的打开操作。而MatSnackbar在打开时会修改自身容器的样式属性(比如你看到的visible-bottom),这就导致Angular在完成一轮变更检测后,发现某个绑定的值从undefined变成了visible-bottom,违反了它的检测规则,于是就抛出了这个错误。
下面给你几个靠谱的解决办法,按简便程度排序:
方法1:用setTimeout延迟snackbar调用(最推荐)
把打开snackbar的代码塞进setTimeout里,让它等当前的变更检测周期结束后再执行——本质是把操作放到了下一个宏任务队列,避开了Angular的检测时机冲突:
ngOnInit() { if (this.route.snapshot.queryParams['returnUrl']) { setTimeout(() => { this.openSnackBar('token expired'); }, 0); } } openSnackBar(message: string, action?: string) { return this.snackBar.open(message, action, { duration: 3000, }); }
方法2:手动触发变更检测
如果你不想用定时器,可以注入ChangeDetectorRef,在打开snackbar后手动告诉Angular“我改了东西,再检测一遍”:
import { ChangeDetectorRef } from '@angular/core'; // 记得在构造函数里注入依赖 constructor( private cdr: ChangeDetectorRef, private route: ActivatedRoute, private snackBar: MatSnackBar ) {} ngOnInit() { if (this.route.snapshot.queryParams['returnUrl']) { this.openSnackBar('token expired'); // 手动触发变更检测,同步视图状态 this.cdr.detectChanges(); } } openSnackBar(message: string, action?: string) { return this.snackBar.open(message, action, { duration: 3000, }); }
方法3:把调用移到ngAfterViewInit钩子
ngOnInit是在组件初始化、但视图还没完全渲染好的时候触发的,而ngAfterViewInit是在视图完全初始化完成后才执行的,这时候初始变更检测已经结束,再打开snackbar就不会有冲突了:
ngAfterViewInit() { if (this.route.snapshot.queryParams['returnUrl']) { this.openSnackBar('token expired'); } } openSnackBar(message: string, action?: string) { return this.snackBar.open(message, action, { duration: 3000, }); }
小提醒
这三个方法里,我最推荐方法1,因为它最省心,不会引入额外的变更检测逻辑,而且0毫秒的延迟用户完全感知不到。方法2适合对性能有极致要求的场景,方法3要注意如果在ngAfterViewInit里做太多操作,可能需要配合cdr.detach()避免重复检测,但对你的场景来说直接用就没问题。
内容的提问来源于stack exchange,提问作者Alessandro Celeghin

