当相应的数组元素发生变化时,如何让 Angular 仅重绘 CSS 网格的元素

问题描述

我已经使用 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 样式。

为了实现这一目标,

  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 创建一个新组件 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 函数按索引进行跟踪
<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 函数。我颠倒了 params 顺序,因此按值而不是按索引进行跟踪。一旦我正确地实现了该功能,一切都按预期工作,而无需进行任何其他更改。

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