所以我试图更好地理解Angulars ChangeDetection,并陷入了一个问题:https://plnkr.co/edit/M8d6FhmDhGWIvSWNVpPm?p=preview
这个Plunkr是我应用程序代码的简化版本,基本上有一个父组件和一个子组件。两者都具有ChangeDettionStrategy。启用OnPush
。
父母亲组成部分ts
@Component({
selector: 'parent',
template: `
<button (click)="click()">Load data</button>
{{stats.dataSize > 0}}
<span *ngIf="stats.dataSize > 0">Works</span>
<child [data]="data" [stats]="stats" (stats)="handleStatsChange()"></child>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent implements OnCheck, OnChanges {
data = [];
stats = {
dataSize: 0
};
constructor(private cdr: ChangeDetectorRef) {
}
click() {
console.log("parent: loading data");
setTimeout(() => {
this.data = ["Data1", "Data2"];
this.cdr.markForCheck();
});
}
handleStatsChange() {
console.log('parent: stats change');
this.cdr.markForCheck();
}
}
小孩组成部分ts
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from "@angular/core";
@Component({
selector: 'child',
template: `
<div *ngFor="let item of data">{{item}}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit, OnChanges {
@Input() data;
@Input() stats;
@Output('stats') statsEmitter = new EventEmitter();
constructor() {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
console.log("child changes: ", changes);
this.stats.dataSize = changes['data'].currentValue.length;
this.statsEmitter.emit(this.stats);
}
}
因此,在单击按钮时,父项更新数据,该按钮触发子项中的ngochanges
。每次数据更改时,child。组件
更改统计信息中的值。我希望在
我注意到:如果我在父对象上删除OnPush
,Angular将抛出一个异常表达式在检查后已更改。上一个值:“false”。当前值:“true”代码>。我猜这是来自
*ngIf=“stats.dataSize
这就是为什么我试着设置这个。指挥官。markForCheck()在父级中的
handleStatsChange
handleStatsChange
将在child中调用。这并没有任何后果,不管怎样抛出异常。
我猜父级上的更改检测不会被触发,因为父级中没有@Input更改,因此ngIf不会更新??点击按钮两次将实际显示Works
。我之所以这么做是因为新的摘要周期现在确实开始了(由事件触发),并且父级ChangeDetectorRef现在正在更新模板?
那么为什么Angular update{{stats.dataSize
任何帮助非常感谢:)
在Angular中,您不能在更改检测周期中更改值。所以,
ngOnChanges(changes: SimpleChanges): void {
console.log("child changes: ", changes);
this.stats.dataSize = changes['data'].currentValue.length; <-- this is not allowed
this.statsEmitter.emit(this.stats);
}
这就是为什么在检查值后会出现更改值的错误。
再深入研究一下生命周期钩子文档,我注意到它们提供了处理更改检测的非常好的示例。
在CounterParentComponent
中的反例中,他们必须通过运行新的“勾号”来更新其日志服务,这意味着使用
setTimeout
延迟执行。
柜台:
updateCounter() {
this.value += 1;
this.logger.tick();
}
记录器:
tick() { this.tick_then(() => { }); }
tick_then(fn: () => any) { setTimeout(fn, 0); }
这正是我必须做的,以使我的代码工作。他们在文件中也提到了这一点:
Angular的单向数据流规则禁止在组成视图后对其进行更新。这两个钩子都是在组件视图组成后触发的。如果钩子立即更新组件的数据绑定注释属性,Angular会抛出一个错误(尝试一下!)。LoggerService.tick_then()将日志更新推迟到浏览器的JavaScript周期的一轮,这已经足够长了。
我今天在intersection observer中遇到了这个问题,将一个项目滚动到视图中本来应该做些什么,99%的时间它做了,但1%的时间它没有启动。
重要提示:异步管道实际上在内部调用markForCheck
。您可以查看源代码(接近末尾)。
我过去使用的一个解决方案是创建一个管道来创建“延迟”。这有助于“表达式更改”错误,因为它有效地导致了新的更改检测器周期。
这应该是最后的选择——但我已经用过好几次了,因为我在别处找不到合适的时机。
<div *ngIf="model.showPanel | delayTime | async">...</div>
这将有一个默认的0ms和延迟()
RxJS管道使用setTimeout:
@Pipe({
name: 'delayTime'
})
export class DelayTimePipe implements PipeTransform {
constructor() {}
transform(value?: Observable<any> | any, ms?: number): Observable<any> {
if (isObservable(value)) {
return value.pipe(delay(ms || 0));
} else {
throw debuggerError('[DelayTimePipe] Needs to be an observable');
}
}
}