Angular 4:Observable触发ExpressionChangedAfterItHasBeenCheckedError,Promise无异常的原因
这个问题本质上是Angular变更检测的执行时机,加上Observable和Promise的回调执行顺序差异导致的,我给你一步步拆解清楚:
首先得先明白这个错误到底在说啥:Angular在开发模式下,每次变更检测完成后都会额外做一次校验,确保组件的模型数据和渲染出来的视图状态是一致的。如果它发现变更检测刚做完,模型数据又被改了,就会抛出这个错误,目的是提前帮你找出可能导致视图显示和实际数据不符的问题。
先看用subscribe为啥会出错
当你在view.component的ngOnInit里调用this.translationService.get('VIEW.title').subscribe(...)的时候,如果是应用内导航的场景,翻译服务其实已经缓存了对应的翻译文本(毕竟之前已经加载过一次了),这时候这个Observable会立刻同步把值发出来——也就是说,subscribe里的回调函数会在ngOnInit执行的过程中直接触发,当场就修改了titleService里的pageTitle值。
那此时的变更检测流程是这样的:
- 路由导航触发了root组件的变更检测
router-outlet开始加载并初始化view.component,执行它的ngOnInit方法- 同步执行的
subscribe回调直接改了titleService.pageTitle - 但
page-title.component已经在这次变更检测周期里完成了视图渲染(它的getPageTitle()已经调用过,拿的是旧的标题值) - Angular在开发模式下的二次校验中,发现
page-title.component显示的旧值和当前模型里的新标题不一致,于是就抛出了ExpressionChangedAfterItHasBeenCheckedError
再看转成toPromise().then为啥没问题
当你把Observable转成Promise并用then回调的时候,then里的代码会被放进微任务队列里——简单说就是,它会等当前所有同步代码都执行完、当前的变更检测周期彻底跑完之后,才会执行这个回调。
流程变成了这样:
- 路由导航触发root组件的变更检测
router-outlet加载并初始化view.component,执行ngOnInittoPromise()把Observable转成Promise,then回调被放进微任务队列排队- 本次变更检测周期结束,所有组件(包括
page-title.component)都已经渲染完成 - 微任务队列开始执行,
then回调修改titleService.pageTitle - Angular的Zone.js会自动捕获到这个异步操作,触发一次新的变更检测,更新
page-title.component的视图 - 因为这次修改是在全新的变更检测周期里做的,不会和上一次检测的结果冲突,所以自然不会报错
为啥刷新页面就不会出错?
你提到刷新页面时不会触发错误,这是因为刷新时整个应用是从头初始化的,翻译服务可能还在加载远程的翻译资源,这时候translationService.get()返回的Observable是异步发值的——哪怕用subscribe,回调也是在当前变更检测周期之后执行的,和Promise的情况类似,所以不会触发错误。而应用内导航时,翻译资源已经缓存好了,Observable同步发值才会引发问题。
除了转Promise,还有其他解决办法吗?
当然有,给你列几个常用的:
- 注入
ChangeDetectorRef,在修改pageTitle之后手动调用cdr.detectChanges(),强制触发一次变更检测 - 把订阅逻辑放到
ngAfterViewInit钩子中(注意要记得取消订阅,避免内存泄漏) - 用
async管道直接在模板里处理Observable(不过这个可能需要调整你的服务结构)
内容的提问来源于stack exchange,提问作者Kir

