开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
在上一篇文章中,我们介绍了一个常见问题,其实上,在日常开发工作中要比例子中的问题复杂很多。我们再来看一个父子组件常见的问题。
error二
父组件:
@Component({
selector: "app-parent",
template: `
{{ count }}
<app-child></app-child>
`,
})
export class ParentComponent {
count = 0
}
子组件:
@Component({
selector: "app-child",
template: ``,
})
export class ChildComponent {
constructor(private parent: ParentComponent) {}
ngOnInit(): void {
this.parent.count++;
}
}
报错: ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
这个错误和上一个例子中的错误一样,上一个例子我们知道在 ngAfterViewInit 中不能改组件的状态。 这个例子中,在子组件的ngOnInit中也不能改变其父元素的状态。
这是为什么呢?
因为,在上面的例子中,ParentComponent 完成变化检测达到稳定状态后(完成了变更检测),ChildComponent 又改变了 ParentComponent 的数据。所以,再次检查数据的时候,发现ParentComponent 数据不一致,就报错了。
那如果我想在子组件中改变父组件的数据,应该怎么办呢?其实在实际场景中,也不会在ngOnInit中去改变父组件的值。
把上面代码改一下。
父组件:
@Component({
selector: "app-parent",
template: `
{{ count }}
<app-child [actionSubject$]="actionSubject$"></app-child>
`,
})
export class ParentComponent {
count = 0;
actionSubject$: Subject<number> = new Subject();
ngOnInit(){
this.actionSubject$.subscribe((data: number) => {
this.count += data;
});
}
}
子组件:
@Component({
selector: "app-child",
template: `<button (click)="changeClick()">Change</button>`,
})
export class ChildComponent {
@Input() actionSubject$!: Subject<any>;
constructor(private parent: ParentComponent) {}
ngOnInit(): void {
}
changeClick() {
// this.parent.count++;
this.actionSubject$.next(1);
}
}
那为什么在onOninit中改变父组件数据会报错呢?这需要了解下变更检测的执行顺序。
变更检测执行顺序
- 更新组件绑定的属性;
- 调用组件生命周期的钩子 ngOnChanges, ngOnInit, ngDoCheck;
- 完成组件变更检测,更新组件的DOM;
- 调用子组件的 ngOnChanges, ngOnInit, ngDoCheck, ngAfterViewInit;
- 在ngAfterViewInit之前完成子组件变更检测,更新组件的DOM;
- 调用组件的生命周期钩子 ngAfterViewInit;
根据上面,我们知道 ngAfterViewInit是在变更检测之后执行的,在父组件执行变化检测后我们更改了父组件的数据,在父组件执行完ngAfterViewInit后,Angular执行开发模式下的第二次检查时,发现与上一次的值不一致,所以报错。
AngularJS采用的是双向数据流,错综复杂的数据流使得它不得不多次检查,使得数据最终趋向稳定。
例子
父组件:
@Component({
selector: "app-parent",
template: `
{{count}}
<app-child [options]="options"></app-child>
`,
})
export class ParentComponent {
count = 0;
options: any = { name: 'Tom' };
ngOnInit(){
}
}
子组件:
@Component({
selector: "app-child",
template: `<h1>{{options.name}}</h1>`,
})
export class ChildComponent {
@Input() options: any;
constructor(private parent: ParentComponent) {}
ngOnInit(): void {
this.options.name = `Jerry`;
}
}
上面代码有什么问题吗?
我们发现它并不会报错,但是违反了单项数据流原则,不建议这么去写。那问题来了,它为什么不会报错?好好思考下哦。
@Component({
selector: "app-parent",
template: `
{{options.name}}
<app-child [options]="options"></app-child>
`,
})
export class ParentComponent {
count = 0;
options: any = { name: 'Tom' };
ngOnInit(){
}
}
一定要注意,参与变更检测的组件状态(也就是在视图中被引用过),才会被checkNoChanges到。
总结
每次触发变化检测,都会从根组件开始,沿着整棵组件树从上到下的执行每个组件的变更检测,默认情况下,直到最后一个叶子 Component 组件完成变更检测达到稳定状态。在这个过程中,一但父组件完成变更检测以后,在下一次事件触发变更检测之前,它的子孙组件都不允许去更改父组件的变化检测相关属性状态的。
是不是现在已经非常清楚这个问题产生的原因了吧。