问题描述
我已经使用 CSS 网格在 Angular 中实现了 Conway's Game of Life。我注意到,即使网格相对较小(50 个方格 x 50 个方格),在单击下一个刻度的按钮和网格更新之间也存在明显的视觉延迟。
我怀疑 Angular 会在每个刻度上重绘所有方块,即使它们中只有一小部分真正发生了变化。我添加了一些淡入淡出的动画,证实了我的怀疑。其实,不完全。有时,某些单元格不会重绘,但它几乎看起来是随机的。大多数单元格处于非活动状态(白色),因此不需要重新绘制。然而,他们中的大多数是,但不是全部。有时几行不会被重绘,其余的会。
我尝试添加一个 trackBy 函数来按索引进行跟踪。这似乎没有帮助。
这是 stackblitz 项目的链接: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 函数。我颠倒了 params 顺序,因此按值而不是按索引进行跟踪。一旦我正确地实现了该功能,一切都按预期工作,而无需进行任何其他更改。
trackByIndex(index: number): number {
return index;
}