ExpressionChangedAfterItHasBeenCheckedError 错误须知,以及变更检测

ExpressionChangedAfterItHasBeenCheckedError 的错误须知

相关变更检测行为

一个运行的Angular程序就是一个组件树,在变更检测期间,Angular会按照以下顺序检查每一个组件,(以下看作列表1)

  • 更新所有子组件/指令的绑定属性
  • 调用所有子组件/指令的三个声明周期:OnChangesngOnInitngDocheck
  • 为子组件执行变更检测(在子组件上重复上面三个步骤,依次递归下去)
  • 为所有子组件/指令调用当前组件的ngAfterViewInit声明周期钩子

这里可以看出ngAfterViewInit钩子是在变更检测之后的。而异步操作会触发Angular的变更检测,所以如果想在ngAfterViewInit钩子中更改子组件属性,可以将其套一层settmeout,变为异步操作
每一次操作后,Angular会记下执行当前操作所需要的值,并存放在组件视图的oldValues属性里(Angular Complier 会把每一个组件编译为对应的 view class,即组件视图类)在所有组件的检查更新操作完成后,Angular并不是马上执行上面列表中操作,而是开始下一次 digest cycle, 即Angular会把来自上一次digest cycle的值与当前值比较(执行下面列表2)

  • 检查已经传给子组件用来更新其属性的值,是否与当前将要传入的值相同。
  • 检查已经传给当前组件用来更新DOM值,是否与当前将要传入的值相同
  • 针对每个子组件执行相同的检查(如果子组件还有子组件,子组件会继续执行上面两部操作,依次递归下去)

这个检查只在开发环境下执行。

举个栗子:假设又一个父组件A 和子组件B, 而A组件有name和text属性,在A组件模板里使用name属性的模板表达式:

//父组件 => A组件
template: '<span>{{name}}</span>'

同时,还有一个B组件,并将A父组件的text属性以输入属性绑定方式传给B组件

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

export class AComponent {
    name = 'I am A component';
    text = 'A message for the child component`;

如果Angular执行变更检测会发生
首先从检查父组件A开始,根据列表1列出的行为,

  1. 第一步更新所有子组件/指令的绑定属性(binding property),所以Angular会计算text表达式的值为A message for the child component,并将值向下传给子组件B,同时Angular还会再当前视图中存储这个值
    view.oldValues[0] = 'A message for the child component';
    
  2. 第二步执行上面列表1 列出的几个声明周期钩子。(即调用子组件B的onChanges,ngOnInit, ngDoCheck)
  3. 第三部是计算模板表达式{{name}}的值为I am A component,然后更新当前组件A的DOM,同时,Angular还会在当前组件视图中存储这个值:
    view.oldValues[1] = 'I am A component';
    
  4. 第四步是为子组件B执行以下第一步到第三步的相同操作,一旦B组件检查完毕,那本次digest loop结束。(因为Angular程序是由组件树构成,当前父组件A组件做了第123步,完后子组件B同样会做第123步,如果B组件还有子组件C,一直递归下去,知道当前树枝的最末端,即最后一个组件没有子组件为止。这一次过程称为digest loop)。

如果处于开发者模式,Angular还会执行列表2列出的digest cycle循环核查。假设当A组件已经把text属性值向下传入给B组件并保存该值后,这是text值图标为updated text,这样Angular运行digest cycle循环核查时,会执行列表2的第一步操作,即检查当前digest cycle的text属性值与上一次时的text属性值是否发生变化:

AComponentView.instance.text === view.oldValues[0]; // false
'A message for the child component' === 'updated text'; // false

结果时发生了变化,此时Angular会抛出 ExpressionChangedAfterItHasBeenCheckedError 错误。
列表 1 中第三步操作也同样会执行 digest cycle 循环检查,如果 name 属性已经在 DOM 中被渲染,并且在组件视图中已经被存储了,那这时 name 属性值突变同样会有同样错误:

AComponentView.instance.name === view.oldValues[1]; // false
'I am A component' === 'updated name'; // false

属性值突变的原因

属性值突变会由子组件或指令引起。
举个栗子:
子组件或指令可以注入到父组件中,假设子组件B注入到它的父组件A,然后更新绑定属性text。在子组件B的ngOnInit生命周期钩子中更新父组件A的属性,这是因为ngOnInit生命周期钩子会在属性绑定完成后触发。

//子组件B
export class BComponent {
    @Input() text;
    constructor(private parent: AppComponent) {}
    ngOnInit() {
        this.parent.text = 'updated text';
    }
}

此时会报错,如果同时改变父组件A的name属性

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

没有报错,在列表1的操作执行顺序中,会发现ngOnInit生命周期钩子会在DOM更新操作执行前触发,所以不会报错

理解单向数据流

如上面所述的每次触发变化检测,都会从根组件,沿着整个组件树从上到下的执行每个组件的变更检测,默认状况下,知道最后一个叶子(Component)组件实现的变更检测达到稳定状态。在整个过程中,一旦父组件实现变更检测当前,在下一次事件触发变更检测之前,他的子孙组件都不容许去更改父组件的变化检测想干属性状态的,这就是单项数据流。

变更检测执行顺序:开发模式为例

  1. 列表1 更新所有子组件/指令的绑定属性
  2. 列表2 检查已经传给子组件用来更新其属性的值,是否与当前将要传入的值相同
  3. 列表2 检查已经传给当前组件用来更新 DOM 值,是否与当前将要传入的值相同
  4. 列表2 针对每一个子组件执行相同的检查(译者注:就是如果子组件还有子组件,子组件会继续执行上面两步的操作,依次递归下去。
  5. 列表1 调用所有子组件/指令的三个生命周期钩子:ngOnInit,OnChanges,ngDoCheck
  6. 列表1 为子组件执行变更检测(译者注:在子组件上重复上面三个步骤,依次递归下去)
  7. 列表1 为所有子组件/指令调用当前组件的 ngAfterViewInit 生命周期钩子

也就是说,假如在父组件中更改子组件的属性,因为变更检测的是单项数据流,会阻止在同一个周期内更新父组件视图,可以通过添加settimeout来等下一轮。

  1. 接父节点发来的Input参数,并把这是的Input参数保存,记作oldInput
  2. 按ngOnChanges, ngOnInit, ngDoCheck的顺序调用自己的声明周期函数
  3. 如果有子组件,将子组件的Input参数下传,并依次调用子组件的ngOnChanges, ngOnInit, ngDoCheck参数
  4. 自身做变化检测,同时更新自己的DOM结构,将此时的DOM结构保存,记为oldDom
  5. 子组件进行变化检测,同时更新dom结构
  6. 子组件调用ngAfterViewInit
  7. 自身调用ngAfterViewInit
  8. 如果是开发模式,会进行第二轮循环,重复1-7
  9. 第二轮循环的1,4步骤会即检测oldInput是否等于input或者oldDom是否等与oldDom,不同就会报ExpressionChangedAfterItHasBeenCheckedError

如何避免

  1. 不要在ngOnChange,ngOnInit,ngDoCheck里面改变父组件下传的input参数
  2. 不要在ngAfterViewInit里改变父组件或自身的dom结构
  3. 可以用异步的方式做变更
  4. 在父组件的ngAfterViewInit最后调用this.cd.detectChanges();(不推荐)

Angular 父子组件的生命周期在这里插入图片描述

参考链接:https://zhuanlan.zhihu.com/p/93268483

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值