angular中ExpressionChangedAfterItHasBeenCheckedError错误

最近在使用ngAfterViewInit的时候发生了一些错误。又发现自己对变更检测的流程其实不是很理解,所以来梳理一下过程,并来讲讲这个错误为什么会发生。
变更检测 首先来了解一下angular对组件的检测操作:
当Angular 对每个组件进行检查,这些组件大致按指定顺序执行的以下操作:

  1. 更新所有子组件/指令的绑定属性 (例如@Input)
  2. 在所有子组件/指令上调用 ngOnInit、OnChanges 和 ngDoCheck 生命周期hook
  3. 更新当前组件的 DOM
  4. 为子组件运行变更检测
  5. 为所有子组件/指令调用 ngAfterViewInit 生命周期钩子
angular中ExpressionChangedAfterItHasBeenCheckedError错误
文章图片

但是在开发模式下会额外执行以下的操作检查:
在每次操作之后,Angular 都会记住它用来执行操作的变量的值。它们存储在组件视图的 oldValues 属性中。
Angular 执行下列的操作:
  • 检查传递给子组件的值是否与oldValues相同
  • 检查用于更新 DOM 元素的值是否与oldValues相同
  • 对所有子组件执行相同的检查
抛出ExpressionChangedAfterItHasBeenCheckedError错误的例子 举个例子:
假设现在是开发模式
定义了A组件,并传递text给B组件
@Component({ selector: 'a-comp', template: ` {{name}} ` })export class AComponent { name = 'Im A' text = 'A to B`; }

@Component({ selector: 'b-comp', }) export class BComponent { @Input() text; constructor(private parent: AComponent) {} ngOnInit() { this.parent.text = 'B to A'; } }

让我们按照的5步变更检测进行:
  1. 更新所有子组件/指令的绑定属性
    angular先执行B组件的text与A组件的text绑定,B组件的text = "A to B"。
    在开发模式下,会记录这个值view.oldValues[0] = '传递给子组件';
  2. 在子组件上调用 ngOnInit等钩子
    此时执行AComponent.text = "B to A";
执行到第二步的时候发生异常,抛出ExpressionChangedAfterItHasBeenCheckedError
如下图
angular中ExpressionChangedAfterItHasBeenCheckedError错误
文章图片

这就是违反了开发模式下的检查所抛出的错误。
简单来说,就是在前一步已经确立好值的情况下,下一步反过来了又将它改变。与oldValue冲突.
即ExpressionChangedAfterItHasBeenCheckedError这个单词的字面意思。
第二个例子
假如我们在B这个子组件中这么做会不会抛出错误呢?
ngOnInit() { this.parent.name = 'updated name'; }

有可能你想:这不也是在ngOnInit中变更父组件的值嘛,肯定会报错。
但结果是不会,原因是什么呢?
别忘了,在A中是这么定义name的:
@Component({ template: ` {{name}} })export class AComponent { name = 'Im A' }

看出点什么了吗? 没错,有关name的操作在第三步才执行。
即:更新本组件的DOM
angular中ExpressionChangedAfterItHasBeenCheckedError错误
文章图片

总结:
其实原理很简单:在前一步已经确立好值后下一步不要更改它,不要与oldValue冲突
项目中的例子 项目中遇到是动态组件方面的报错例子,也是抛出ExpressionChangedAfterItHasBeenCheckedError。
简单介绍一下代码:
export class App { @ViewChild(FormItemDirective, {static: true}) appFormItem: FormItemDirective; constructor(private r: ComponentFactoryResolver) { }ngAfterViewInit() { const f = this.r.resolveComponentFactory(BComponent); this.appFormItem.viewContainerRef.createComponent(f); } }

根据5步流程,报错的原因就很简单了:
该组件在 ngAfterViewInit 中动态添加一个子组件。由于添加子组件需要修改 DOM,并且在 Angular 更新 DOM 后触发 ngAfterViewInit 生命周期钩子,又去修改DOM,因此会引发错误。
如图:第五步时又去修改第三步已经确立的DOM。
angular中ExpressionChangedAfterItHasBeenCheckedError错误
文章图片

解决方法 这里以项目中的修改为例子,讲讲如何解决第5步中修改了第3步中已经确立的DOM发生的问题。
1. 把修改提前 很简单的方法,既然是在第三步中确立的DOM, 那么在第一步和第二步中修改它不就行了。
  • 在第一步中修改,可以使用@Input,因为更新子组件的绑定属性是在第一步中完成。
    @Input() set setValue(value: Type) { // do someting }

  • 在第二步中修改,ngOnInit等钩子是在第二步中完成
    ngOnInit() { // do something }

2. 强制变更检测 另一种可能的解决方案是为父 A 组件强制执行另一个更改检测周期。最好的地方是在 ngAfterViewInit 生命周期钩子中,因为它是在对所有子组件执行更改检测时触发的,因此它们比较可能更新父组件属性。
使用ChangeDetectorRef.detectChanges()就能完成这个操作。
export class AppComponent { constructor(private cd: ChangeDetectorRef) { } ngAfterViewInit() { this.cd.detectChanges(); }

几个问题 为什么angular需要这么验证?
【angular中ExpressionChangedAfterItHasBeenCheckedError错误】Angular 强制执行所谓的从上到下的单向数据流。处理父级更改后,不允许层次结构较低的组件更新父组件的属性.
这确保了在变更检测之后,整个组件树是稳定的。如果需要与依赖于这些属性的消费者同步的属性发生变化,则树是不稳定的。
为什么只在开发模式下运行它?
可能因为集成模式不像开发模式运行时错误那样严重。毕竟它可能会在下一次摘要运行中稳定下来。
但是,最好在开发模式下就解决它,而不是留给客户端来尝试调试它。
参考文章:https://hackernoon.com/everyt...

    推荐阅读