最近在stackoverflow上彷佛天天都有一些关于angular报错‘ExpressionChangedAfterItHasBeenCheckedError’的问题。发生这些问题一般是因为angular的开发者不懂angular变动检测的工做原理,以及为何这个检测的报错是有必要的。不少开发者甚至认为这是angular的bug。但其实不是的。这是一个用于防止模型数据和ui之间数组不一致的一个警告机制,以便不让用户在页面上看到陈旧的或者错误的数据。javascript
这篇文章解释了该报错和检测机制的潜在缘由,提供了几种可能引起报错的通用模式和可能的修复方案。文章最后解释了为何检测机制是重要的。
1、相关变动检测操做
一个运行中的angular应用是一个组件树。在变动检测期间,angular按照如下的特定顺序检查每个组件:
一、更新全部子组件/指令绑定的特性
二、调用全部的自组件/指令的ngOnInit, OnChanges和ngDocCheck生命周期勾子
三、更新当前组件的DOM视图
四、为一个子组件运行变动检测
五、调用全部子组件/指令的ngAfterViewInit生命周期勾子
每一步操做后,angular会记住每一步用于操做的值,它们会被保存在控制器视图的oldValues属性中,在对全部的组件进行检查后,angular进入下一个摘要周期(原词是digest cycle,这里不知道怎么翻译更准确),而不是执行上面列表中的操做,它将当前值与上一个摘要周期中保存的值进行比较,过程以下:
一、检查传递给子组件的值与将用于更新这些组件属性的值相同
二、检查用于用于更新dom元素的值与将用于更新这些元素的值相同
三、对全部子组件执行相同的检查
请注意该额外的检查只会在开发模式下执行,我在文章最后一段已经解释了缘由。
让咱们看一个例子,假设你有一个父组件a和子组件b,a组件有一个‘name’变量和一个‘text’属性,在该例子模版中使用引用名称属性的表达式:
template: '<span>{{name}}</span>'
而且该例子模版中还有一个b控制器,其经过输入属性绑定将text属性传递给该组件。
@Component({
selector: 'a-comp',
template: `
{{name}}
`
})
export class AComponent {
name = 'I am A component';
text = 'A message for the child component`;
因此当angular运行变动检测时会发生什么。变动检测从检查a组件开始。上述列表中第一步是更新绑定的属性,以便获得text表达式值为“A message for the child component”,并将该值传递给b组件,最后保存到控制视图的oldValues属性中:
view.oldValues[0] = 'A message for the child component';
而后开始调用上述列表中第二步提到的生命周期勾子。
如今开始执行第三步操做以及计算出表单式{{name}}的值为“I am A component”,angular使用该值更新dom视图而且将该值保存到oldValues:
view.oldValues[1] = 'I am A component';
接着angular执行下一步操做,而且为b控制器运行相同的检查。一旦b控制器变动检测完毕,当前的摘要循环即结束。
若是angular是在开发环境下运行的,将会执行第二次的摘要来执行上述列表中的步骤。如今想象一下,在angular把text的值“A message for the child component”传递给b组件而且保存到控制器视图中的oldValues后,text变量值在a组件上被以某种方式更新为“updated text”,如今angular运行验证摘要,而后第一步操做是检查属性text有无改变:
AComponentView.instance.text === view.oldValues[0]; // false
'A message for the child component' === 'updated text'; // false
显而易见,text改变了,也就是在摘要周期中检查传递给子组件的值与将用于更新这些组件属性的值不相同了,因此抛出了ExpressionChangedAfterItHasBeenCheckedError错误。
一样,若是name属性值在被呈现和存储后更新,也会获得相同的报错:
AComponentView.instance.name === view.oldValues[1]; // false
'I am A component' === 'updated name'; // false
你如今可能有一个疑问,即这些值是如何改变的,让咱们往下看。
2、值改变的可能场景
一般致使变化的罪魁祸首每每是组件或者指令。让咱们来简单快速的演示一下。我会使用尽量最简单的例子,可是以后就要开始展现真实的场景了。你应该知道,子组件和指令可以注入其父组件,所以让咱们来将b组件注入到a组件中而且更新其绑定的属性text,咱们将会在ngOnInit生命周期勾子中更新该属性值,由于ngOnInit是在绑定完成以后被触发的,代码以下:
export class BComponent {
@Input() text;
constructor(private parent: AppComponent) {}
ngOnInit() {
this.parent.text = 'updated text';
}
}
不出所料,咱们获得了报错:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'A message for the child component'. Current value: 'updated text'.
如今,咱们用a组件表达式中的name属性作一样的操做:
ngOnInit() {
this.parent.name = 'updated name';
}
如今一切正常,怎么回事?
若是你仔细观察上述变动检测的执行顺序就会发现ngOnInit生命周期勾子是在dom更新操做以前被触发的,这就是为何上面没有获得报错的缘由。咱们如今须要一个在dom更新操做完成以后的生命周期勾子,好比ngAfterViewInit就很是适合:
export class BComponent {
@Input() text;
constructor(private parent: AppComponent) {}
ngAfterViewInit() {
this.parent.name = 'updated name';
}
}
此次咱们又获得了预料中的错误:
AppComponent.ngfactory.js:8 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'I am A component'. Current value: 'updated name'.
固然,真实场景的案例是错综复杂的,那些致使dom渲染的父组件属性更新或操做每每是经过服务或者可观察对象模式间接实现的,可是根本原理和缘由是相同的。
如今,让咱们看一些真实场景下致使错误的共同模式。
一、共享服务
该应用设计为在父组件和子组件之间共享一个服务。子组件为服务设置一个值,继而经过更新父组件的属性实现反应,我称这个父属性的更新为间接的,由于与上面的例子不一样,如今子组件更新父组件属性不是很是显著的。
二、同步事件广播(Synchronous event broadcasting)
该应用设计为一个子组件发送一个事件,而后父组件监听这个事件,该事件会致使父组件一些属性值的更新。同时这些属性被用于子组件的输入绑定中。这也是一个间接的父组件属性更新。
三、动态组件实例化(Dynamic component instantiation)ngAfterViewInit
这种模式不一样于以前输入绑定受到影响,而是会引发dom更新操做抛出错误。该应用设计为在父组件的ngAfterViewInit中动态的添加一个子组件,因为添加子组件须要dom修改,dom更新后继而触发ngAfterViewInit生命周期钩子,抛出错误。
3、可能的修复解决方案
若是你看一下以下的错误描述
Expression has changed after it was checked. Previous value:…
不由思考,它是在变化检测勾子中建立的吗?java
一般,修复方案即经过正确的变动检测机制来建立动态组件。例如上面章节中的最后一个例子,能够将动态组件的建立过程移到ngOnInit生命周期勾子中,尽管文档说明ViewChild只能在ngAfterViewInit以后使用,可是在建立视图的时候,它归属于子组件,所以能够更早使用。express
若是你用谷歌搜索相关资料,你可能会发现针对这个报错的两个最大众化的解决方案 —— 异步属性更新和强制附加变化检测周期,尽管我把这两个解决方案放在了这里,同时还解释了它们的工做原理,可是我不推荐使用这些方案,而是应该从新设计你的应用。这一点我会在文章末尾阐述缘由。
一、异步属性更新
这里须要注意的是,变动检测和验证摘要是同步执行的,这意味着若是咱们异步更新属性,当验证循环正在运行中时,属性值不会变化更新,应用也就不会抛出错误了,让咱们试一下:
export class BComponent {
name = 'I am B component';
@Input() text;
constructor(private parent: AppComponent) {}
ngOnInit() {
setTimeout(() => {
this.parent.text = 'updated text';
});
}
ngAfterViewInit() {
setTimeout(() => {
this.parent.name = 'updated name';
});
}
}
的确没有抛出错误,setTimeout函数调度了宏任务,而后将在下面的vm回调中执行,可是须要在当前同步代码完成以后经过使用promise回调来执行。
Promise.resolve(null).then(() => this.parent.name = 'updated name');
替代宏任务promise.then来建立一个微任务,在当前同步代码完成执行以后,微任务队列被处理,所以在验证步骤以后将发生对属性的更新。了解更多angular中的宏任何和微任务,能够前往I reverse-engineered Zones (zone.js) and here is what I’ve found.(https://blog.angularindepth.com/i-reverse-engineered-zones-zone-js-and-here-is-what-ive-found-1f48dc87659b)
若是你在使用EventEmitter,你能够传递true参数选项来设置异步机制 数组
new EventEmitter(true);
二、强制更新检测
另外一个可能的解决方案是在第一种方案和认证阶段之间,为父级的a组件强制增长一个变动检测周期,执行这一方案最好的地方是在ngAfterViewInit生命周期勾子中,由于它是在给全部子组件执行变动检测时被触发,因此有可能会更新父组件的属性。
export class AppComponent {
name = 'I am A component';
text = 'A message for the child component';
constructor(private cd: ChangeDetectorRef) {
}
ngAfterViewInit() {
this.cd.detectChanges();
}
}
没有报错,彷佛正确工做了,可是该解决方案存在一个问题,当触发对父组件的更新检测时,angular将运行对全部子组件的变动检测,会存在父组件属性更新的可能。
4、为何须要验证环
Angular从上到下强制执行所谓的单向数据流。 在父级更改处理完毕后,层级中较低的组件不容许更新父组件的属性。 这确保了在第一个摘要循环以后整个组件树是稳定的。 若是须要与依赖于这些属性的使用者同步的属性发生更改,则树不稳定。 在咱们的例子中,一个B子组件依赖于父文本属性。 只要这些属性发生更改,组件树就会变得不稳定,直到将此更改传递给子组件B。 DOM也是如此。 它是组件上某些属性的使用者,它在UI上呈现它们。 若是某些属性未同步,用户将在页面上看到不正确的信息。
这个数据同步过程就是变化检测过程当中发生的状况 - 特别是我在开始时列出的两个操做。 那么,若是在同步操做执行后更新子组件属性的父属性,会发生什么状况? 对,你留下了不稳定的树,这种状态的后果是不可能预测的。 大多数状况下,您最终会在页面上向用户显示不正确的信息。 这将很难调试。
那么为何不运行变化检测直到组件树稳定? 答案很简单 - 由于它可能永远不会稳定下来并永远运行。 若是一个子组件更新父组件上的一个属性,做为对该属性更改的反应,则会发生无限循环。 固然,正如我以前所说的,使用直接更新或依赖关系来发现这种模式是微不足道的,但在实际应用程序中,更新和依赖关系一般是间接的。
有趣的是,AngularJS没有单向数据流,所以它试图稳定树。 但它一般会致使臭名昭着的10 $ digest()迭代达成。停止! 错误。 继续,谷歌这个错误,你会惊讶于这个错误产生的问题的数量。
最后一个问题是为何只在开发模式下运行它? 我想这是由于一个不稳定的模型并不像框架产生的运行时错误那样严重。 毕竟它可能稳定在下一个摘要运行。 可是,开发应用程序时可能出现的错误比在客户端上调试正在运行的应用程序更好。
译自:Everything you need to know about the `ExpressionChangedAfterItHasBeenCheckedError` errorpromise
https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4