Angular 使用未定义副作用的 trackby 函数

Angular trackby function using undefined side effects

我在我的一个组件中经常遇到更改检测 运行 的问题,因此我尝试对 ngFor 指令使用 trackBy 选项。

通过阅读,我了解到 Angular 将使用从您的 trackyBy 函数中编辑的值 return,因为它在下次更改检测运行时会有所不同。为了看看它是否符合我的需要,并尝试更好地理解它,我建立了一个游乐场。在使用的时候,我把我用的trackyBy函数的return值设为returnundefined,还是得到了我想要的结果

TS:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  styleUrls: ['./app.component.scss'],
  templateUrl: './app.component.html',
})
export class AppComponent {
collection;
  constructor() {
    this.collection = [{id: 1, value: 0}, {id: 2, value: 0}, {id: 3, value: 0}];
  }

  getItems() {
    this.collection = this.getItemsFromServer();
  }

  getItemsFromServer() {
    return [{id: 5, value: 0}, {id: 2, value: 0}, {id: 3, value: 3}, {id: 4, value: 4}];
  }

  trackByFn(index, item) {
    return undefined;
  }
}

HTML:

    <ul>
      <li *ngFor="let item of collection;trackBy: trackByFn">{{ item.id }}hello {{item.value}}</li>
    </ul>
    <button (click)="getItems()">Refresh items</button>

第一次点击的结果是,除数组的索引 1 外,所有项目都使用其新值或 ID 重新呈现。第二次单击时,none 项重新呈现,因为对象内没有任何变化。

所以我的问题是,为什么有人会为 trackBy 函数的 return 值使用唯一 ID?必须有一些我遗漏的东西,我不希望它以我还没有看到的方式影响我的应用程序。

官方答案是您使用 trackBy 来避免在 DOM 中为具有相同标识符的对象的新实例重新创建元素。

从表面上看,您的设置并不能证明 ngFor 不只是忽略您在 trackByFn 中返回的 undefined 值并重新创建 DOM 模型中出现新实例时仍然存在元素。

与现有对象具有相同 ID 的新项目可能更改了其他属性,因此无论您是否使用(或误用)[=10],您都希望 HTML 是正确的=].

设置

我使用您的代码创建了一个测试环境,除了我分叉了 *ngFor 的源代码,以便我可以添加自己的日志记录来跟踪 *ngFor.

中发生的事情

我测试了三种场景:

A) trackByFn returns 唯一id

B) trackByFn returns undefined

C) 不使用 trackBy

我跟踪了以下步骤中每个场景中发生的情况

  1. 创建列表
  2. 部分替换部分列表数据
  3. 排序列表
  4. 重置列表数据

我在 "pure" 测试的每一步都分配了新的对象实例。

结果

1.创建列表

所有 3 种情况都相同 - 为列表中的每个项目创建一个 DOM 元素。

2。部分替换部分列表数据

A) 删除已删除项目的 DOM 元素并为新项目创建新的 DOM 元素。所有元素都已更新。

B) 为索引超出原始数组范围的项目创建新的 DOM 元素。所有元素都已更新。

C) 重新创建所有 DOM 个元素。

3。排序列表

A) 移动移动位置

的DOM个元素

B) 更新移动位置

的DOM个元素

C) 重新创建所有 DOM 个元素

4.重置列表数据

A) 删除已删除项目的 DOM 元素并为新项目创建新的 DOM 元素。所有元素都已更新(与方案 2 相同)。

B) 删除已删除项目的 DOM 个元素。所有元素都已更新。

C) 重新创建所有 DOM 个元素。

结论

重要的是要注意这些测试是使用对象的新实例完成的。 *ngFor 如果您重用对象引用,效率会更高。

如果你有一个非常不稳定的列表,使用 trackBy 在 DOM 操作方面更有效。

令人惊讶的结果

从我的测试来看,与从 trackByFn 返回唯一标识符时相比,您的示例所做的 DOM 操作似乎更少。如果您用 3 个新项目替换 3 个项目,您的方法将不会执行任何 DOM 操作,并且仍然 运行 与 "proper" 方式相同的更新方法。 "proper" 方法将删除原来的 3 个 DOM 元素并添加 3 个新的 DOM 元素。

这表明我们可以只提供一个 trackByFn returns 一个常量值而不会出现任何意外结果。通过查看源代码并试用它,我看不出这是怎么回事(除了让其他查看您代码的人感到困惑)。

这确实让我想知道为什么默认实现必须重新创建所有 DOM 元素,而重用旧的 DOM 元素似乎工作得很好。我敢肯定有些情况我没有考虑过,我很想听听。

演示版:https://stackblitz.com/edit/angular-anejhw

这变成了一些 "fun" 研究任务,而不是一个明确的答案,但希望它被证明是有用的。尽管我已经证明从 trackByFn 返回一个常量值似乎是性能最高的选项,但我仍然对在生产代码中使用这种方法犹豫不决。即使它现在适用于所有情况,如果它在某个时候成为 "fixed" 作为一个错误,我也不会感到惊讶。

ngForOf源代码:https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_for_of.ts

TLDR;不要 return undefined 或 trackBy 中的其他常量。这并不意味着什么,无论您需要什么,都有更好的选择。

ngFor 的实现细节非常复杂,我不会为了这个答案而剖析它们。此外,我们真的不需要分析每一个细节。我们只需要了解 trackBy:

的目的是什么

trackBy 是一种简化可迭代源中项目跟踪的方法,以便 (1) 优化性能和 (2) 防止破坏可以保持活动状态的项目.

(1) 很明显:DOM 操作越少,性能越好。 (2) 没那么多:如果我想为列表中的元素设置动画(例如,在为排序操作设置动画时移动元素,或显示 :enter:leave 动画)我真的需要实例化的组件是"properly" 已跟踪。如果我没有正确设置 trackBy,这些项目将被不必要地销毁和重新创建,而不是正确地四处移动。

为了更好地展示这一点,我设置了一个示例来说明这一点:

https://stackblitz.com/edit/angular-dnh2ti

每个 ngFor 生成 AppCounter 个实例,能够跟踪构造函数调用的累积次数并显示一个 name 值。换句话说,我们可以跟踪组件是否被重新创建以及组件输入是否被重置。

第一个按钮更改 name 绑定的值。否 ngFor 重新创建组件,因为项目默认由对象实例跟踪。

第二个按钮(创建新实例)显示即使我们完全改变实例,从头开始重建数组中的对象,我们也可以告诉 ngFor 组件管理组件创建更聪明的方法。即:复杂值的计数器 - 没有 trackBy 在每次单击按钮时重新创建所有组件; trackByUndefined 也是如此,而 trackByProperty 正确地防止了组件破坏。如果您需要为进入和离开设置动画,或者如果组件创建成本很高,这一点至关重要。对于具有 onPush changeDetectionStrategy.

的项目组件尤其如此

trackByProperty 方法是一行:

trackByPropertyName(idx: number, value: any) {
  // use ?. on new TypeScript versions
  return value && value.name;
}

第二个按钮也随机化数组randomValues。这用于显示 trackByProperty 方法的另一种替代方法:trackByIndex.

如果您真的想让所有组件按照它们呈现的相同顺序排列,并且您只想更新数据,一个简单但有效的选择是使用:

trackByIndex(idx: number, _value: any) {
  return idx;
}

这显然会在排序时阻止动画,但它会保留实例化的组件 "alive",即使值完全改变也是如此。

这个答案已经很长了,几乎是一个博客post,但我想重申一点:使用trackBy告诉ngFor使用什么策略来优化组件实例化。该函数通常是单行的,只是 returning 一个 id 属性 或行的索引,所以这样做不会增加复杂性,但它会提高性能和您 ngFor 的行为更好,更可预测。只有当你有简单的值或者你根本不关心性能时才使用默认值(但是,真的,谁不关心?)。