0 Tage Serie
javascript / expert
Snippet

Strukturelle Gleichheitswächter für OnPush-Komponentenbäume

OnPush-Komponenten rendern immer dann neu, wenn ein `@Input()` eine neue Objektreferenz erhält — auch wenn dieses Objekt strukturell mit dem vorherigen identisch ist. Das ist eine häufige Quelle verschwendeter Change-Detection-Zyklen in Angular-Bäumen, in denen Eltern-Komponenten Filter- oder Konfigurationsobjekte bei jedem Render neu aufbauen. Die Kapselung des Setters in eine strukturelle Gleichheitsprüfung durchbricht diese Kopplung an die Referenzidentität: Der Input-Setter läuft bei jeder Referenzänderung, aber die teure Neuberechnung erfolgt nur, wenn sich die tatsächlichen Feldwerte unterscheiden. Das unterscheidet sich von `distinctUntilChanged` auf einem Observable-Stream, da hier ein einfaches `@Input()`-Binding ohne Operator-Pipeline abgesichert wird.

snippet.js
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
interface Filters {
status: readonly string[];
minPrice: number;
}
function sameFilters(a: Filters, b: Filters): boolean {
return (
a.minPrice === b.minPrice &&
a.status.length === b.status.length &&
a.status.every((s, i) => s === b.status[i])
);
}
@Component({
selector: 'app-catalog',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<app-list [items]="visibleItems" />`,
})
export class CatalogComponent {
private lastFilters: Filters | null = null;
visibleItems: unknown[] = [];
@Input() set filters(next: Filters) {
if (this.lastFilters && sameFilters(this.lastFilters, next)) {
return;
}
this.lastFilters = next;
this.visibleItems = this.recompute(next);
}
private recompute(f: Filters): unknown[] {
return [];
}
}
angular
Erklärung
1
if (this.lastFilters && sameFilters(this.lastFilters, next)) { return; }
Bricht vor der Neuzuweisung von `visibleItems` früh ab, sodass keine neue Array-Referenz entsteht und die Kind-Komponente `app-list` ihre eigene OnPush-Prüfung überspringt.
2
a.status.length === b.status.length && a.status.every((s, i) => s === b.status[i])
Vergleicht Array-Inhalte anhand des Index statt der Referenz, da upstream neu aufgebaute `readonly string[]`-Arrays selbst bei identischen Werten nie `===` sind.
3
@Input() set filters(next: Filters) { ... }
Angular ruft einen Input-Setter bei jeder Referenzänderung auf; die Wächter-Logik muss darin liegen, da Angular keine eingebaute Gleichheitsprüfung für Inputs bietet.