Angular OnPush 更改检测传播到 ngFor 循环中的子组件

Angular OnPush Change Detection Propagation to a Child Components in a ngFor Loop

我在 Angular 应用程序中遇到 onPush 更改检测问题。

我创建了一个演示应用程序来说明问题: https://stackblitz.com/edit/angular-vcebqu

该应用程序包含一个 parent 组件和一个 child 组件。

parentchild 都在使用 onPush 变化检测。

parentchild 都将输入分解为 getter 和 setter,this.cd.markForCheck(); 在 setter 中使用。


    private _element: any;

    @Output()
    elementChange = new EventEmitter<any>();

    @Input()
    get element() {
        return this._element;
    }

    set element(newVal: any) {
        if (this._element === newVal) { return; }
        this._element = newVal;
        this.cd.markForCheck();
        this.elementChange.emit(this._element);
    }

parent 组件使用 *ngFor 循环创建多个 child 组件,如下所示:


<app-child 
    *ngFor="let element of item.elements; let index = index; trackBy: trackElementBy" 
    [element]="item.elements[index]"
    (elementChange)="item.elements[index]=$event"></app-child>

问题是,如果数据在 parent 组件中更新,则更改不会传播到 child 组件。

在演示应用程序中,单击 'change' 按钮并注意 'elements' 数组 ( elements[0].order ) 中的第一个 'element' 在父级中更新,但是该更改未显示在第一个 child 组件的 'element' 中。但是,如果从 child 组件中删除 OnPush 变化检测,它可以正常工作。

您必须将 @Input() 装饰器添加到 setter 方法。

get element() {
    return this._element;
}

@Input()
set element(newVal: any) {
    this._element = newVal;
}

还有一些其他的东西:

  • Angular 不会设置重复值,因为 OnPush 仅在输入更改时才设置输入。
  • 不要在setter中调用this.cd.markForCheck(),因为组件已经脏了。
  • 您不必从输入中输出值。父组件已经知道输入了什么。

由于传递给子组件的输入不是数组,因此 IterableDiffers 将不起作用。 KeyValueDiffers however can be used in this case to watch for changes in the input object and then handle it accordingly (stackblitz link):

  import {
  Component,
  OnInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  KeyValueDiffers,
  KeyValueDiffer,
  EventEmitter,
  Output, Input
} from '@angular/core';


@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {

  private _element: any;

  @Output()
  elementChange = new EventEmitter<any>();


  get element() {
    return this._element;
  }

  @Input()
  set element(newVal: any) {
    if (this._element === newVal) { return; }
    this._element = newVal;
    this.cd.markForCheck();
    this.elementChange.emit(this._element);
  }

  private elementDiffer: KeyValueDiffer<string, any>;

  constructor(
    private cd: ChangeDetectorRef,
    private differs: KeyValueDiffers
  ) {
    this.elementDiffer = differs.find({}).create();
  }

  ngOnInit() {
  }

  ngOnChanges() {
    // or here
  }

  ngDoCheck() {
    const changes = this.elementDiffer.diff(this.element);
    if (changes) {
      this.element = { ...this.element };
    }
  }
}

只需添加替代解决方案,以防其他人遇到此问题。 ChildComponent 未在其模板中反映新值的主要原因是因为只有 'element' 的 属性 'order' 正在从父组件更改,因此父组件正在注入相同的内容修改后的 'order' 属性 的对象引用。

OnPush 更改检测策略将仅在将新对象引用注入组件时 'detect changes'。因此,为了让 ChildComponent(具有 OnPush 变化检测策略)触发变化检测,您必须向 "element" 输入 属性 注入一个新的对象引用,而不是相同的

要查看实际效果,请打开 https://stackblitz.com/edit/angular-vcebqu 并进行 ff 更改。

on file parent.component.ts, modify the method onClick($event) {...} to:

onClick(event){
  const random = Math.floor(Math.random() * (10 - 1 + 1)) + 1;
  this.item.elements[0] = {...this.item.elements[0], order: random};
}

最后一行将数组中索引 0 处的对象引用替换为与数组中旧的第一个元素相同的新对象,除了顺序 属性.