最近在应用 ngAfterViewInit 的时候产生了一些谬误。又发现自己对变更检测的流程其实不是很了解,所以来梳理一下过程,并来讲讲这个谬误为什么会产生。
变更检测
首先来理解一下 angular 对组件的检测操作:
当 Angular 对每个组件进行查看,这些组件大抵按指定程序执行的以下操作:
- 更新所有子组件 / 指令的绑定属性 (例如 @Input)
- 在所有子组件 / 指令上调用 ngOnInit、OnChanges 和 ngDoCheck 生命周期 hook
- 更新以后组件的 DOM
- 为子组件运行变更检测
- 为所有子组件 / 指令调用 ngAfterViewInit 生命周期钩子
然而在开发模式下会额定执行以下的操作查看:
在每次操作之后,Angular 都会记住它用来执行操作的变量的值。它们存储在组件视图的 oldValues 属性中。
Angular 执行下列的操作:
- 查看传递给子组件的值是否与 oldValues 雷同
- 查看用于更新 DOM 元素的值是否与 oldValues 雷同
- 对所有子组件执行雷同的查看
抛出 ExpressionChangedAfterItHasBeenCheckedError 谬误的例子
举个例子:
假如当初是开发模式
定义了 A 组件,并传递 text 给 B 组件
@Component({
selector: 'a-comp',
template: `
<span>{{name}}</span>
<b-comp ="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 步变更检测进行:
- 更新所有子组件 / 指令的绑定属性
angular 先执行 B 组件的 text 与 A 组件的 text 绑定,B 组件的 text = “A to B”。
在开发模式下,会记录这个值 view.oldValues[0] = ‘ 传递给子组件 ’; - 在子组件上调用 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…