如何让 Angular 仅在 CSS 网格的相应数组元素发生更改时重绘它们
How to get Angular to only redraw elements of a CSS grid when their corresponding array element has changed
我在 Angular 中使用 CSS 网格实现了 Conway 的生命游戏。我注意到,即使使用相对较小的网格(50 平方 x 50 平方),在单击下一个刻度按钮和网格更新之间也存在明显的视觉滞后。
我怀疑 Angular 每次都重新绘制所有正方形,即使只有一小部分真正发生变化。我添加了一些褪色动画,证实了我的怀疑。实际上,不完全是。有时,某些单元格不会重绘,但它看起来几乎是随机的。大多数单元格是不活动的(白色),因此不需要重新绘制。然而,他们中的大多数是,但不是全部。有时不会重绘几行,其余的会。
我尝试添加一个 trackBy 函数来按索引进行跟踪。这似乎没有帮助。
这里是 stackblitz 项目的 link:https://stackblitz.com/edit/angular-conway?file=src/app/app.component.ts
代码如下:
<div
style="display: grid; grid-template-columns: repeat(50, 15px); grid-template-rows: repeat(50, 15px)"
>
<div
*ngFor="let alive of grid; index as i"
class="my-animation"
[ngStyle]="{'border': 'solid black 1px', 'background-color': alive ? 'black' : 'white'}"
></div>
</div>
<button (click)="nextTick()">Next</button>
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { getNeighbors } from "../../get-neighbors";
@Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
grid: boolean[] = [];
ngOnInit(): void {
this._randomStart(0.1);
}
private _randomStart(probabilityOfAlive: number): void {
for (let i = 0; i < 2500; i++) {
this.grid[i] = Math.random() <= probabilityOfAlive;
}
}
nextTick(): void {
this._updateGrid();
}
private _updateGrid(): void {
const newGrid = [];
for (let i = 0; i < 2500; i++) {
const isAlive = this.grid[i];
const liveNeighborsCount = this._getLiveNeighborsCount(i);
if (isAlive && (liveNeighborsCount === 2 || liveNeighborsCount === 3))
newGrid[i] = true;
else if (!isAlive && liveNeighborsCount === 3) newGrid[i] = true;
else newGrid[i] = false;
}
this.grid = newGrid;
}
private _getLiveNeighborsCount(cellIndex: number): number {
return getNeighbors({ index: cellIndex, width: 50, heigth: 50 }).reduce(
(sumOfAlive, indexOfNeighbor) => {
return this.grid[indexOfNeighbor] ? sumOfAlive + 1 : sumOfAlive;
},
0
);
}
}
是的,你是对的。有一个延迟,因为 Angular 每个刻度重新绘制所有网格单元格。这是因为您要求它在您的模板中这样做:每次 grid
数组更改时,都会重新绘制 DOM 单元格(这需要时间),因为 ngFor
循环执行。
为避免完全重绘网格,请考虑仅绘制一次网格,并在每次更新时简单地更新 CSS 单元格样式。
要做到这一点,
- 在您的组件中,添加一个包含 2500 个空元素的虚拟静态数组,
ngFor
循环将使用这些元素仅绘制一次网格:
cells = Array(2500);
- 以这种方式更改您的模板:
<div *ngFor="let cell of cells; index as i"
class="my-animation"
[ngStyle]="{'border': 'solid black 1px', 'background-color': grid[i] ? 'black' : 'white'}">
</div>
在此处查看实际效果:https://stackblitz.com/edit/angular-conway-h8wvfj?file=src/app/app.component.ts
我会像下面这样处理它
- 使用 html
创建一个新组件 grid-item-component
<div
class="my-animation"
[ngStyle]="{'border': 'solid black 1px', 'background-color': isAlive ? 'black' : 'white'}"
>
</div>
和 TS
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GridItemComponent {
@Input('is-alive') isAlive
}
想法是只要输入 isAlive 发生变化,这个组件就会更新,否则不会更新,因为我们使用的是 ChangeDetectionStrategy.OnPush
- 更改html,不要忘记包含
trackBy
功能。我们将使用 trackByIndex
函数 按索引进行跟踪
<button (click)="nextTick()">Next</button>
<div
style="display: grid; grid-template-columns: repeat(50, 15px); grid-template-rows: repeat(50, 15px)"
>
<app-grid-item
*ngFor="let alive of grid; index as i; trackBy:trackByIndex"
[is-alive]='alive'
></app-grid-item>
</div>
- 在您的 TS 文件中添加以下函数
trackByIndex = (i) => i
下面是一个sample demo
最后,我最初的实现唯一有问题的是 trackBy 函数。我颠倒了参数顺序,因此按值而不是索引进行跟踪。一旦我正确地实现了该功能,一切都按预期工作,而无需进行任何其他更改。
trackByIndex(index: number): number {
return index;
}
我在 Angular 中使用 CSS 网格实现了 Conway 的生命游戏。我注意到,即使使用相对较小的网格(50 平方 x 50 平方),在单击下一个刻度按钮和网格更新之间也存在明显的视觉滞后。
我怀疑 Angular 每次都重新绘制所有正方形,即使只有一小部分真正发生变化。我添加了一些褪色动画,证实了我的怀疑。实际上,不完全是。有时,某些单元格不会重绘,但它看起来几乎是随机的。大多数单元格是不活动的(白色),因此不需要重新绘制。然而,他们中的大多数是,但不是全部。有时不会重绘几行,其余的会。
我尝试添加一个 trackBy 函数来按索引进行跟踪。这似乎没有帮助。
这里是 stackblitz 项目的 link:https://stackblitz.com/edit/angular-conway?file=src/app/app.component.ts
代码如下:
<div
style="display: grid; grid-template-columns: repeat(50, 15px); grid-template-rows: repeat(50, 15px)"
>
<div
*ngFor="let alive of grid; index as i"
class="my-animation"
[ngStyle]="{'border': 'solid black 1px', 'background-color': alive ? 'black' : 'white'}"
></div>
</div>
<button (click)="nextTick()">Next</button>
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { getNeighbors } from "../../get-neighbors";
@Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
grid: boolean[] = [];
ngOnInit(): void {
this._randomStart(0.1);
}
private _randomStart(probabilityOfAlive: number): void {
for (let i = 0; i < 2500; i++) {
this.grid[i] = Math.random() <= probabilityOfAlive;
}
}
nextTick(): void {
this._updateGrid();
}
private _updateGrid(): void {
const newGrid = [];
for (let i = 0; i < 2500; i++) {
const isAlive = this.grid[i];
const liveNeighborsCount = this._getLiveNeighborsCount(i);
if (isAlive && (liveNeighborsCount === 2 || liveNeighborsCount === 3))
newGrid[i] = true;
else if (!isAlive && liveNeighborsCount === 3) newGrid[i] = true;
else newGrid[i] = false;
}
this.grid = newGrid;
}
private _getLiveNeighborsCount(cellIndex: number): number {
return getNeighbors({ index: cellIndex, width: 50, heigth: 50 }).reduce(
(sumOfAlive, indexOfNeighbor) => {
return this.grid[indexOfNeighbor] ? sumOfAlive + 1 : sumOfAlive;
},
0
);
}
}
是的,你是对的。有一个延迟,因为 Angular 每个刻度重新绘制所有网格单元格。这是因为您要求它在您的模板中这样做:每次 grid
数组更改时,都会重新绘制 DOM 单元格(这需要时间),因为 ngFor
循环执行。
为避免完全重绘网格,请考虑仅绘制一次网格,并在每次更新时简单地更新 CSS 单元格样式。
要做到这一点,
- 在您的组件中,添加一个包含 2500 个空元素的虚拟静态数组,
ngFor
循环将使用这些元素仅绘制一次网格:
cells = Array(2500);
- 以这种方式更改您的模板:
<div *ngFor="let cell of cells; index as i"
class="my-animation"
[ngStyle]="{'border': 'solid black 1px', 'background-color': grid[i] ? 'black' : 'white'}">
</div>
在此处查看实际效果:https://stackblitz.com/edit/angular-conway-h8wvfj?file=src/app/app.component.ts
我会像下面这样处理它
- 使用 html 创建一个新组件 grid-item-component
<div
class="my-animation"
[ngStyle]="{'border': 'solid black 1px', 'background-color': isAlive ? 'black' : 'white'}"
>
</div>
和 TS
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GridItemComponent {
@Input('is-alive') isAlive
}
想法是只要输入 isAlive 发生变化,这个组件就会更新,否则不会更新,因为我们使用的是 ChangeDetectionStrategy.OnPush
- 更改html,不要忘记包含
trackBy
功能。我们将使用trackByIndex
函数 按索引进行跟踪
<button (click)="nextTick()">Next</button>
<div
style="display: grid; grid-template-columns: repeat(50, 15px); grid-template-rows: repeat(50, 15px)"
>
<app-grid-item
*ngFor="let alive of grid; index as i; trackBy:trackByIndex"
[is-alive]='alive'
></app-grid-item>
</div>
- 在您的 TS 文件中添加以下函数
trackByIndex = (i) => i
下面是一个sample demo
最后,我最初的实现唯一有问题的是 trackBy 函数。我颠倒了参数顺序,因此按值而不是索引进行跟踪。一旦我正确地实现了该功能,一切都按预期工作,而无需进行任何其他更改。
trackByIndex(index: number): number {
return index;
}