如何让 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 单元格样式。

要做到这一点,

  1. 在您的组件中,添加一个包含 2500 个空元素的虚拟静态数组,ngFor 循环将使用这些元素仅绘制一次网格:
cells = Array(2500);
  1. 以这种方式更改您的模板:
<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

我会像下面这样处理它

  1. 使用 html
  2. 创建一个新组件 grid-item-component
  <div
    class="my-animation"
    [ngStyle]="{'border': 'solid black 1px', 'background-color':  isAlive ? 'black' : 'white'}"
  >
  &nbsp;
  </div>

和 TS

  import { ChangeDetectionStrategy, Component, Input,  OnInit } from '@angular/core';

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class GridItemComponent {
  @Input('is-alive') isAlive
}

想法是只要输入 isAlive 发生变化,这个组件就会更新,否则不会更新,因为我们使用的是 ChangeDetectionStrategy.OnPush

  1. 更改html,不要忘记包含trackBy功能。我们将使用 trackByIndex 函数
  2. 按索引进行跟踪
<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>
  1. 在您的 TS 文件中添加以下函数
trackByIndex = (i) =>  i

下面是一个sample demo

最后,我最初的实现唯一有问题的是 trackBy 函数。我颠倒了参数顺序,因此按值而不是索引进行跟踪。一旦我正确地实现了该功能,一切都按预期工作,而无需进行任何其他更改。

trackByIndex(index: number): number {
  return index;
}