Angular ChangeDetection NgIf

Angular ChangeDetection NgIf

所以我试图更好地理解 Angulars ChangeDetection 并无意中遇到了一个问题: https://plnkr.co/edit/M8d6FhmDhGWIvSWNVpPm?p=preview

这个 Plunkr 是我的应用程序代码的简化版本,基本上有一个 parent 和一个 child 成分。 两者都启用了 ChangeDetectionStrategy.OnPush

parent.component.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();
    }
}

child.component.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);
    }
}

因此 parent 会在点击按钮时更新 data,这会触发 child 中的 ngOnChanges。 每次数据更改时,child.component 都会更改 stats 中的值。 我想要这个值 dataSize 用于 parent 中的 <span *ngIf="stats.dataSize > 0">Works</span>。 出于某种原因,*ngIf 不会更新。模板{{stats.dataSize > 0}}否则更新没问题

我注意到了什么: 如果我在 parent 上删除 OnPush,Angular 将抛出异常 Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.。 我猜这是因为 *ngIf="stats.dataSize > 0" 首先是 false,现在在开发模式下的变化检测的第二次迭代之后是 true。

这就是为什么我尝试在 handleStatsChange 中将 parent 设置为 this.cdr.markForCheck(); 的原因。 handleStatsChange 将在 child 中调用。这没有任何后果,无论如何都会抛出异常。

我想 parent 上的变化检测不会被触发,因为 parent 中没有更改 @Input,因此 ngIf 不会更新??单击按钮 两次 次实际上将显示 Works。这是因为新的摘要周期现在开始(由事件触发)并且 parents ChangeDetectorRef 现在正在更新模板?

那么为什么 Angular 更新 {{stats.dataSize > 0}} 并在 ngIf 处抛出错误?

非常感谢任何帮助:)

在 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);
    }

这就是为什么您收到有关值在检查后被更改的错误。

深入研究 Lifecycle Hooks Documentation 我注意到它们提供了有关处理变更检测的非常好的示例。

在他们 CounterParentComponent 的反例中,他们必须通过 运行 更新他们的 LoggerService 一个新的 'tick',这意味着用 setTimeout 延迟执行。

柜台:

updateCounter() {
    this.value += 1;
    this.logger.tick();
}

记录器:

tick() {  this.tick_then(() => { }); }
tick_then(fn: () => any) { setTimeout(fn, 0); }

这正是我必须做的才能让我的代码正常工作。他们还在文档中提到了这一点:

Angular's unidirectional data flow rule forbids updates to the view after it has been composed. Both of these hooks fire after the component's view has been composed. Angular throws an error if the hook updates the component's data-bound comment property immediately (try it!). The LoggerService.tick_then() postpones the log update for one turn of the browser's JavaScript cycle and that's just long enough.

我今天在 intersection observer 中遇到了这个问题,在这个问题中,将一个项目滚动到视图中应该会做一些事情,99% 的时间它会做,但 1% 的时间它不会触发。

重要提示:异步管道实际上在内部调用 markForCheck。可以看看the source code(接近尾声)

我过去使用 observables 的一个解决方案是创建一个创建 'delay' 的管道。这有助于 'expression changed' 错误,因为它有效地导致了新的变化检测器周期。

这应该是最后的手段 - 但我已经使用了几次,因为我无法在其他地方获得正确的时间。

<div *ngIf="model.showPanel | delayTime | async">...</div>

默认值为 0ms,delay() 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');
        }
    }
}