开发环境 ‘ExpressionChangedAfterItHasBeenCheckedError’。
在开发环境中我们会遇到上面的这个错误,就是因为angular2的检测机制。
参考文档(本文的很多内容也是翻译或者理解此文而来):
Everything you need to know about the `ExpressionChangedAfterItHasBeenCheckedError` error
问题背景:
我们在用angular2的时候有时候会遇到这个错误,但是如果发布到到生产环境的话,错误就不存在,程序正常运行。
Angular is running in the development mode. Call enableProdMode() to enable the production mode.
这句话是我们在使用angular2的时候控制台常见的提示,当然按照这句话的说法,我们可以在开发环境中使用enableProdMode(),会执行和开发环境的一样的检测机制。
import{enableProdMode }from'@angular/core';
enableProdMode();
这样的话可以最简单的解决这个报错。生产环境去掉这个就可以了。
这当然不是angular2的一个bug,这是一个警戒机制,以防止模型数据和UI之间的不一致,从而不会向页面上的用户显示错误或旧的数据。开发环境和生产环境的检测机制是不一样的,在开发环境,当数据模型和ui不一致的可能就会报这个错误。
angular2的相关变更检测操作
运行的Angular应用程序是一个组件树。 在更改检测期间,Angular对每个组件执行检查,该组件由按照指定顺序执行的以下操作组成:
------更新所有子组件/指令的绑定属性
------调用所有子组件/指令的ngOnInit,OnChanges和ngDoCheck生命周期钩子
------更新当前组件的DOM
------运行子组件的更改检测
------调用所有子组件/指令的ngAfterViewInit生命周期钩子
每次操作后,Angular会记住用于执行操作的值。 它们存储在组件视图的oldValues属性中。 对所有组件进行检查后,Angular将开始下一个摘要循环,但不是执行上面列出的操作,而是将当前值与之前摘要循环中记录的值进行比较:
------检查传递给子组件的值与现在用于更新这些组件的属性的值是否相同
------检查用于更新DOM元素的值是否与现在用于更新这些元素的值是否相同
------对所有子组件执行相同的检查
下面也引用文章里的例子,有A组件父组件,B组件子组件,并A组件有text传值到B组件,B组件@Input()接收。
@Component({
selector: 'a-comp',
template: `{{name}}`}
)
export class AComponent {
name = 'I am A component';
text = 'A message for the child component`;
}
下面是当Angular运行更改检测时会发生什么。
它首先检查A组件。 列表中的第一个操作是更新绑定,以便将文本表达式计算为子组件的A消息,并将其传递给B组件。 它还将此值存储在视图上:
view.oldValues[0] = 'A message for the child component';
然后Angular执行下一个操作并对子B组件运行相同的检查。 一旦B组件被检查,当前的检测循环就完成了。
如果Angular在开发模式下运行,则运行上面列出的第二个摘要执行验证操作。 现在想象一下,在Angular将子组件的值A消息传递给B组件并将其存储之后,属性文本在A组件上更新为更新的文本。 所以现在运行验证摘要,第一个操作是检查属性文本是否没有更改。
这时候发现改变了就会保持前面的错误。
重现1.当前这个做demo可以在A组件的AfterCheced里改变
ngAfterViewChecked() {
this.text="new2 - A message for the child component";
}
这时候就会抛出前面的提到的错误。
重现2.另一个我们可以通过在子组件里注入父组件,然后改变父组件的text值,这时候是已经完成了绑定,然后进行的修改,同样也会报错相同的错误,认为你的值有变化, 也就是在绑定存储完之后。
B组件里
import { AComponent } from ‘./aComponent'
export class BComponent {
@Input() text;
constructor(private parent: AComponent) {}
ngAfterViewInit() {
this.parent.name = 'New';
}
}
这时候问题就会重现。
当然实际的开发当中的情况不是这么简单的。一般开发的时候的数据可能是通过服务获得。当然不出这个问题更好。出这个问题的话我们就要想到解决方法。
下面我们看一下解决方案:
1. 也是上面提到的。直接添加enableProdMode()
2. 将第二个的重现里改成异步的,加定时器也会发现不报错了。当前实际环境可不是这样的。
ngAfterViewInit() {
setTimeout( ()=>{
this.parent.text='New';
})
}
实际环境没这么简单,具体这种方法怎么应用到实际环境还有待研究。
3. 从第一个重现我们看到在检测完之后改变是会报错的,实际这个问题也就是检测完之后数据发生变化引起的,实际上我们可以通过代码可以让angular2强制执行检测。
链接文章里说在AfterviewInit里加,不过我是用了没效果,加在AfterViewChecked里起作用的。
import{Component, OnInit, Input, ChangeDetectorRef } from '@angular/core';
export class AComponent {
constructor(private cd: ChangeDetectorRef) {}
ngOnIOnit(){}
ngAfterViewChecked() {
this.cd.detectChanges();
}
}
4. 这个就根据实际情况来了,有时候这个问题的产生可能是因为自己DOM结构不合理引起的,比如要给子组件传的数据是不是依赖于页面上这个DOM节点之后的元素的绑定的数据。
换一下位置可能就解决了,当然如果这个位置不能变的话就得在考虑其他方式了。
网友评论