likes
comments
collection
share

React key属性:性能列表的最佳实践

作者站长头像
站长
· 阅读数 11

重点内容:

  • key只在重新渲染时生效
  • key一致时,React不会移除,重新挂载DOM。也就不会触发组建的render和mounted事件。同时组件内部维护的state也能维持。只会根据props更新DOM的显示内容
  • key不一致时,React会移除组件。重新挂载DOM。所有组件内部state会被重置。

使用建议

  • 不要在“key”属性中使用随机值:这会导致每次重新渲染时都重新挂载项目。当然,除非这是你有意这么做。
  • 在“静态”列表中(项目数量和顺序保持不变的列表)中使用数组的索引作为“key”是没有问题的。
  • 当列表可以重新排序或随机插入项目时,使用项目的唯一标识符(如“id”)作为“key”。
  • 对于动态列表中的无状态项,可以使用数组的索引作为“key”,例如分页列表、搜索和自动完成结果等。这将提高列表的性能。

React的"key"属性可能是React中最常使用的性能优化手段之一 😅 老实说,有多少人可以坦言他们使用它是因为使用它可以带来对应的性能提升,而不是“因为ESLint规则限制”。我怀疑当人们面对“为什么React需要“key”属性”的问题时,大多数人会回答类似于: “嗯...我们应该在那里放入唯一的值,以便React可以识别列表项,这样做对性能更好”。从技术上讲,这个回答是正确的。恩,有时候....

但是,“识别列表项”究竟意味着什么呢?如果我省略了“key”属性会发生什么?应用程序会崩溃吗?如果我在那里放入一个随机字符串会怎样?这个值应该有多唯一?我可以只使用数组的索引值吗?这些选择的影响是什么?它们对性能有什么具体的影响,以及为什么会有这些影响?

让我们一起来探究一下!

以下正文

key属性是如何工作的

首先,在着手编码之前,让我们理清概念:什么是“key”属性,以及为什么React需要它。

简而言之,如果“key”属性存在,React在重新渲染过程中使用它来标识相同类型的元素,以便在其兄弟元素中进行区分。(请参阅文档:reactjs.org/docs/lists-…reactjs.org/docs/reconc… )。换句话说,它仅在重新渲染时并且在相邻的相同类型元素(即扁平列表)之间才需要使用(这很重要!)。

在重新渲染过程中,简化的算法如下:

  • 首先,React会生成元素的“before”和“after”状态的“快照”。
  • 其次,它将尝试识别那些已经存在于页面上的元素,以便在不需要重新创建的情况下重新使用它们。
    • 如果“key”属性存在,React会认为在“before”和“after”两个状态下,具有相同Key属性的项是相同的项。
    • 如果“key”属性不存在,它将使用索引作为默认的“key”。
  • 然后,它会执行以下操作:
    • 删除在“before”阶段存在但在“after”中不存在的项(即卸载它们)。
    • 从头开始创建在“before”中不存在的项(即挂载它们)。
    • 更新在“before”中存在并在“after”中继续存在的项(即重新渲染它们)。

通过实际编码过程更容易理解,所以我们也来尝试一下。

为什么使用随机值作为key不是个好主意

让我们先实现一个国家列表。我们需要创建一个Item组件,用来渲染国家信息

const Item = ({ country }) => {
  return (
    <button className="country-item">
      <img src={country.flagUrl} />
      {country.name}
    </button>
  );
};

同时创建一个CountriesList组件用来渲染国家列表

const Item = ({ country }) => {
  return (
    <button className="country-item">
      <img src={country.flagUrl} />
      {country.name}
    </button>
  );
};

眼下我们还没有在Item上设置key属性。设想一下,如果此时CountriesList组件重新渲染,会发生什么?

  • React将会发现,当前列表Item没有使用key,便会将国家数组的索引(index)作为默认的key。
  • 由于数组没有发生变化,所有的项将被标识为“已经存在”,并且这些项将会重新渲染。

从本质上讲,这与在Item中明确添加key={index}没有区别。

countries.map((country, index) => <Item country={country} key={index} />);

React key属性:性能列表的最佳实践 简而言之:当CountriesList组件重新渲染的时候,每个Item也会重新渲染。如果使用React.memo包裹Item组件,那我们甚至可以规避掉这些不必要的渲染以提升数组的性能。

有意思的地方在于:如果不使用index而是用随机数作为key,会怎么样?

countries.map((country, index) => <Item country={country} key={Math.random()} />);

在这种情况下:

  • CountriesList组件每次重新渲染,React会为每个Item生成新的随机数作为key。
  • 因为key存在,那么React会使用已经存在的key来标注这个Item.
  • 既然每个key都是新的,所有前一个状态中的Item会被认为已经被删除。新状态下所有的item都被认为是新的,React就会卸载所有的Item并重新挂载他们。

React key属性:性能列表的最佳实践

简而言之:当 CountriesList 组件重新渲染时,每个 Item 都将被销毁并从头开始重新创建。

当我们讨论性能的时候,相较于简单的重新渲染,重新挂载毫无疑问是昂贵多的开销。同时包裹React.memo带来的所有性能提升都将没有意义。memo优化对挂载不会生效。

查看codesandbox上的案例,点击按钮触发重新渲染,同时关注控制台的输出。把你的CPU的性能调到很低,点击按钮时的延迟甚至可以用肉眼看到!

如何调低CPU性能

在Chrome开发者工具中打开 "性能 "选项卡,点击右上角的 "齿轮 "图标--它将打开一个附加面板,"CPU节流 "是其中一个选项。

为什么index作为key属性的值不是个好主意

到现在,在重新渲染的时候,为什么我们需要一个稳定的key值就显而易见了。那么数组的索引呢?哪怕是在官方文档上,都不推荐这么做。因为这样会导致bug和性能问题。但是,当我们使用index代替唯一的ID作为key时,究竟发生了什么会导致这样的结果呢?

首先,我们在上面的示例中看不到这些内容。所有的bug和性能影响只会发生在动态列表中 - 既那些重新渲染时,数量或者排序会改变的数组。为了模拟这一点,让我们为列表增加一个排序功能:

const CountriesList = ({ countries }) => {
  // introduce some state
  const [sort, setSort] = useState('asc');

  // sort countries base on state value with lodash orderBy function
  const sortedCountries = orderBy(countries, 'name', sort);

  // add button that toggles state between 'asc' and 'desc'
  const button = <button onClick={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>toggle sorting: {sort}</button>;

  return (
    <div>
      {button}
      {sortedCountries.map((country) => (
        <ItemMemo country={country} />
      ))}
    </div>
  );
};

我将用两种方式实现列表,其中使用country.id作为key:

sortedCountries.map((country) => <ItemMemo country={country} key={country.id} />);

另一种使用index作为key:

sortedCountries.map((country, index) => <ItemMemo country={country} key={index} />);

出于性能的目的,我们使用React.memoItem进行包裹。

const ItemMemo = React.memo(Item);

完整实现参见Codesandbox。点击排序按钮,可以观察到,在降低 CPU 速度的情况下,"index" 作为key的列表稍微慢一些。 并注意控制台输出:在 "index" 作为key的列表中,每个项目都会在每次按钮点击时重新渲染,即使 Item 组件已经被 memo包裹, 从技术上不应该发生这种情况。而基于 "id" 的实现(它与基于 "index" 的实现完全相同,除了 key 值不同)就不会出现这个问题:在按钮点击后,没有任何项目重新渲染,并且控制台输出是干净的。

那么到底发生了什么?显然秘密就藏着key值中:

  • React生成“before”和“after”元素列表,并尝试识别“相同”的项。
  • 从React的角度来看,具有相同键的项被认为是“相同的”项。
  • 在“index”为基础的实现中,数组中的第一项始终具有key="0",第二项具有key="1",以此类推,无论数组如何排序。

因此,当React进行比较时,当它在“before”和“after”列表中都看到具有key="0"的项时,它认为它们是完全相同的项,只是props值不同:在我们颠倒数组后,country值发生了变化。因此,它会按照相同项的处理方式进行处理:触发重新渲染周期。由于它认为country prop值已经改变,它将绕过memo函数并触发实际项的重新渲染。

React key属性:性能列表的最佳实践 基于ID的行为是正确且高效的:项被准确地识别,并且每个项都被memoized,因此没有组件会重新渲染。

React key属性:性能列表的最佳实践 如果我们为Item组件引入一些状态,这种现象就会尤其明显。例如,让我们在单击时更改其背景颜色:

const Item = ({ country }) => {
  // add some state to capture whether the item is active or not
  const [isActive, setIsActive] = useState(false);

  // when the button is clicked - toggle the state
  return (
    <button className={`country-item ${isActive ? 'active' : ''}`} onClick={() => setIsActive(!isActive)}>
      <img src={country.flagUrl} />
      {country.name}
    </button>
  );
};

查看相同的Codesandbox,但这次先点击一些国家,触发背景色的变化,然后再点击“sort”按钮。

基于ID的列表的表现和我们预期的完全一致。但是基于index的列表现在表现得很奇怪:如果我点击列表中的第一项,然后点击排序按钮 - 无论排序如何,第一项仍然保持选中状态。这就是上面描述的行为的症状:React认为具有key="0"(数组中的第一项)的项在状态变化前后是完全相同的,因此它重复使用同一个组件实例,保持状态不变(即isActive设置为true),只更新props的值(从第一个国家变为最后一个国家)。

如果我们不进行排序,而是在数组的开头添加一项,也会出现完全相同的情况:React会认为具有key="0"的项(第一项)保持不变,而最后一项是新添加的项。因此,如果第一项被选中,在基于索引的列表中,选中状态将保持在第一项上,每个项都会重新渲染,并且最后一项会触发“mount”事件。在基于ID的列表中,只有新添加的项会被挂载和渲染,其他项则保持不变。可以在Codesandbox中查看此情况。降低CPU速度,即可肉眼可见地观察到在基于index的列表中添加新项的延迟!即使在6倍CPU限制下,基于ID的列表也运行非常快速。

为什么将“index”作为“key”属性是个好主意

在前面的部分中,我们可以轻松地说“始终使用唯一的项目ID作为“key”属性”,对吗?在大多数情况下,这是正确的,如果您始终使用ID,可能没有人会注意或介意。但是,当您拥有这个知识时,您就拥有了超能力。现在,由于我们知道了React在渲染列表时发生了什么,我们可以通过使用索引而不是ID来提高某些列表的性能。

一个典型的场景是分页列表。您有一个有限数量的项目列表,当您点击按钮时,希望在相同大小的列表中显示相同类型的不同项目。如果您选择key="id"的方法,那么每次更改页面时,您将加载完全不同ID的全新项目集。这意味着React无法找到任何“已存在”的项目,会卸载整个列表,并重新挂载全新的项目集。但是!如果您选择key="index"的方法,React将认为新“页面”上的所有项目都已经存在,并且只会使用新数据更新这些项目,保持实际组件的挂载状态。即使在相对较小的数据集上,如果项目组件很复杂,这种方法也会明显提高性能。

请在Codesandbox中查看此示例。请注意控制台输出 - 当您在右侧的“id”基于列表中切换页面时,每个项目都会重新挂载。但在左侧的“index”基于列表中,项目只会重新渲染。速度更快!即使在带有50个非常简单列表(只包含文本和图像)的情况下,使用限制的CPU,切换页面在“id”基于列表和“index”基于列表之间的差异已经是可见的。

在各种动态列表类似的数据情况下,情况完全相同,您在保留列表样式的同时用新数据集替换现有项目:自动完成组件、类似Google搜索的页面、分页表格等。只需要注意在这些项目中引入状态:它们要么是无状态的,要么状态应与props同步。

所有的key都需要在正确的位置

以下是一些要点总结:

  • 不要在“key”属性中使用随机值:这会导致每次重新渲染时都重新挂载项目。当然,除非这是你有意这么做。

  • 在“静态”列表中(项目数量和顺序保持不变的列表)中使用数组的索引作为“key”是没有问题的。

  • 当列表可以重新排序或随机插入项目时,使用项目的唯一标识符(如“id”)作为“key”。

  • 对于动态列表中的无状态项,可以使用数组的索引作为“key”,例如分页列表、搜索和自动完成结果等。这将提高列表的性能。

    祝您有个美好的一天,愿您的列表项只有在您明确要求的情况下才会重新渲染!✌🏼

原文地址