Распространение обнаружения изменений Angular OnPush на дочерние компоненты в цикле ngFor

У меня проблема с обнаружением изменений onPush в приложении Angular.

Я создал демонстрационное приложение, которое иллюстрирует проблему: https://stackblitz.com/edit/angular-vcebqu

Приложение содержит parent компонент и child компонент.

И parent, и child используют обнаружение изменений onPush.

И parent, и child имеют входы, разбитые на геттеры и сеттеры, причем this.cd.markForCheck(); используется в установщиках.


    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 создает несколько компонентов child, используя цикл *ngFor, например:


<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.

В демонстрационном приложении нажмите кнопку «изменить» и обратите внимание, что первый «элемент» в массиве «elements» (elements[0].order) обновляется в родительском элементе, но изменение не отображается в «элементе» первого child компонента. Однако, если обнаружение изменений OnPush удалено из компонента child, оно работает правильно.


person jptrue    schedule 07.02.2019    source источник


Ответы (3)


Поскольку входные данные, переданные дочернему компоненту, не являются массивом, IterableDiffers не будет работать. Однако KeyValueDiffers в этом случае можно использовать для отслеживания изменений во входном объекте и последующей обработки. соответственно (ссылка на stackblitz):

  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 };
    }
  }
}
person rahotimus    schedule 11.02.2019
comment
Отличное решение! Огромное спасибо. - person jptrue; 11.02.2019

Просто добавьте альтернативное решение на случай, если кто-то другой столкнется с этой проблемой. Основная причина, по которой ChildComponent не отражает новое значение в своем шаблоне, заключается в том, что из родительского компонента изменяется только свойство «порядок» для «элемента», поэтому родительский компонент вводит ту же ссылку на объект с измененным «порядком». имущество.

Стратегия обнаружения изменений OnPush будет «обнаруживать изменения» только тогда, когда в компонент вводится новая ссылка на объект. Таким образом, для того, чтобы ChildComponent (имеющий стратегию обнаружения изменений OnPush) инициировал обнаружение изменений, вы должны вставить новую ссылку на объект во входное свойство "element" вместо того же самого.

Чтобы увидеть это в действии, откройте https://stackblitz.com/edit/angular-vcebqu и внесите изменения ff.

в файле parent.component.ts измените метод onClick($event) {...} на:

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

Последняя строка заменяет ссылку на объект внутри массива с индексом 0 на новый объект, идентичный старому первому элементу в массиве, за исключением свойства order.

person SoulRider    schedule 20.06.2020

Вы должны добавить декоратор @Input() к методу установки.

get element() {
    return this._element;
}

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

Также вот еще кое-что:

  • Angular не будет устанавливать повторяющиеся значения, потому что OnPush устанавливает входные данные только тогда, когда они были изменены.
  • Не вызывайте this.cd.markForCheck() в сеттере, потому что компонент уже загрязнен.
  • Вам не нужно выводить значение из входа. Родительский компонент уже знает, что было введено.
person Reactgular    schedule 07.02.2019
comment
Спасибо cgTag. Я попробовал ваши предложения здесь: stackblitz.com/edit/angular-bc4tc7, к сожалению, они не сделали работать на меня. - person jptrue; 07.02.2019