关于angular:angular中ExpressionChangedAfterItHasBeenCheckedError错误

最近在应用ngAfterViewInit的时候产生了一些谬误。又发现自己对变更检测的流程其实不是很了解,所以来梳理一下过程,并来讲讲这个谬误为什么会产生。

变更检测

首先来理解一下angular对组件的检测操作:

当Angular 对每个组件进行查看,这些组件大抵按指定程序执行的以下操作

  1. 更新所有子组件/指令的绑定属性 (例如@Input)
  2. 在所有子组件/指令上调用 ngOnInit、OnChanges 和 ngDoCheck 生命周期hook
  3. 更新以后组件的 DOM
  4. 为子组件运行变更检测
  5. 为所有子组件/指令调用 ngAfterViewInit 生命周期钩子


然而在开发模式下会额定执行以下的操作查看:

在每次操作之后,Angular 都会记住它用来执行操作的变量的值。它们存储在组件视图的 oldValues 属性中。

Angular 执行下列的操作:

  • 查看传递给子组件的值是否与oldValues雷同
  • 查看用于更新 DOM 元素的值是否与oldValues雷同
  • 对所有子组件执行雷同的查看

抛出ExpressionChangedAfterItHasBeenCheckedError谬误的例子

举个例子:

假如当初是开发模式

定义了A组件,并传递text给B组件

@Component({ 
selector: 'a-comp',
template: ` 
 <span>{{name}}</span>
 <b-comp [text]="text"></b-comp> `
 })

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
如下图

这就是违反了开发模式下的查看所抛出的谬误。

简略来说,就是在前一步曾经确立好值的状况下,下一步反过来了又将它扭转。与oldValue抵触.

即ExpressionChangedAfterItHasBeenCheckedError这个单词的字面意思。


第二个例子

如果咱们在B这个子组件中这么做会不会抛出谬误呢?

ngOnInit() { this.parent.name = 'updated name'; }

有可能你想:这不也是在ngOnInit中变更父组件的值嘛,必定会报错。

但后果是不会,起因是什么呢?

别忘了,在A中是这么定义name的:

@Component({ 
template: ` 
 <span>{{name}}</span>
 })

export class AComponent { 
    name = 'Im A'
}

看出点什么了吗? 没错,无关name的操作在第三步才执行。

即:更新本组件的DOM

总结:

其实原理很简略:在前一步曾经确立好值后下一步不要更改它,不要与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。

解决办法

这里以我的项目中的批改为例子,讲讲如何解决第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 强制执行所谓的从上到下的单向数据流。解决父级更改后,不容许层次结构较低的组件更新父组件的属性.

这确保了在变更检测之后,整个组件树是稳固的。如果须要与依赖于这些属性的消费者同步的属性发生变化,则树是不稳固的。

为什么只在开发模式下运行它?

可能因为集成模式不像开发模式运行时谬误那样重大。毕竟它可能会在下一次摘要运行中稳定下来。

然而,最好在开发模式下就解决它,而不是留给客户端来尝试调试它。


参考文章:https://hackernoon.com/everyt…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理