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
组件。
parent
和 child
都在使用 onPush 变化检测。
parent
和 child
都将输入分解为 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 处的对象引用替换为与数组中旧的第一个元素相同的新对象,除了顺序 属性.
我在 Angular 应用程序中遇到 onPush 更改检测问题。
我创建了一个演示应用程序来说明问题: https://stackblitz.com/edit/angular-vcebqu
该应用程序包含一个 parent
组件和一个 child
组件。
parent
和 child
都在使用 onPush 变化检测。
parent
和 child
都将输入分解为 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 methodonClick($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 处的对象引用替换为与数组中旧的第一个元素相同的新对象,除了顺序 属性.