发布于 2026-01-06 0 阅读
0

React 关键属性:高性能列表的最佳实践

React 关键属性:高性能列表的最佳实践

图片描述

本文最初发表于https://www.developerway.com。该网站还有更多类似文章 😉


React 的“key”属性可能是 React 中最常用的“自动驾驶”特性之一了😅。我们当中又有谁会真心实意地说自己使用它是因为“……一些合理的理由”,而不是“因为 ESLint 规则报错了”呢?我怀疑大多数人在被问到“为什么 React 需要“key”属性”时,都会回答类似“呃……我们应该在那里放置唯一值,这样 React 才能识别列表项,性能也更好”。从技术上讲,这个答案有时是正确的。

但“识别项目”究竟是什么意思?如果我省略“key”属性会发生什么?应用程序会崩溃吗?如果我在那里放一个随机字符串呢?这个值应该有多独特?我可以直接使用数组的索引值吗?这些选择会带来什么影响?它们具体会如何影响性能,为什么?

我们一起来调查吧!

React 的 key 属性是如何工作的?

首先,在开始编码之前,让我们先弄清楚理论:什么是“key”属性以及为什么 React 需要它。

简而言之,如果存在“key”属性,React 会使用它在重新渲染期间识别同类型元素(参见文档:https://reactjs.org/docs/lists-and-keys.htmlhttps://reactjs.org/docs/reconciliation.html#recursing-on-children)。换句话说,它仅在重新渲染期间以及对于相同类型的相邻元素(即扁平列表)时才需要(这一点很重要!)。

重新渲染过程中的简化算法如下所示:

  • 首先,React 会生成元素的“之前”和“之后”的“快照”。
  • 其次,它会尝试识别页面上已存在的元素,以便重用它们而不是从头开始创建。
    • 如果存在“key”属性,则假定具有相同“before”和“after”键的项是相同的。
    • 如果“key”属性不存在,则会使用同级元素的索引作为默认“key”。
  • 第三,它将:
    • 删除“之前”阶段存在但在“之后”阶段不存在的项目(即卸载它们)。
    • 从零开始创建之前版本中不存在的物品(例如,挂载它们)
    • 更新“之前”已存在且“之后”仍然存在的项目(即重新渲染它们)

稍微动手操作一下代码就更容易理解了,所以我们也来试试吧。

为什么随机生成“键”属性是个坏主意?

我们先来实现国家列表。我们会有一个Item组件来渲染国家的信息:

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

以及一个CountriesList用于渲染实际列表的组件:

const CountriesList = ({ countries }) => {
  return (
    <div>
      {countries.map((country) => (
        <Item country={country} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

目前我的组件还没有“key”属性。那么CountriesList组件重新渲染时会发生什么呢?

  • React 会发现这里没有“key”,因此会回退到使用countries数组的索引作为键。
  • 我们的数组没有改变,因此所有元素都会被识别为“已存在”,并且这些元素会被重新渲染。

本质上,这与明确地增加key={index}内容并无不同。Item

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

图片描述

简而言之:当CountriesList组件重新渲染时,所有元素Item也会随之重新渲染。如果我们用 `<div>` 标签包裹Item起来React.memo,甚至可以避免这些不必要的重新渲染,从而提升列表组件的性能。

现在有趣的部分来了:如果我们不在“key”属性中添加索引,而是添加一些随机字符串,会怎么样呢?

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

在这种情况下:

  • 每次重新渲染时CountriesList,React 都会重新生成“key”属性。
  • 由于存在“key”属性,React 将使用它来识别“已存在”的元素。
  • 由于所有“key”属性都将是新的,因此之前的所有项目都将被视为“已移除”,每个项目都Item将被视为“新”,React 将卸载所有项目并重新挂载它们。

图片描述

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

重新挂载组件的开销要大得多,远高于简单的重新渲染,尤其是在性能方面。此外,所有通过封装元素带来的性能提升都React.memo将消失——由于每次重新渲染都会重新创建元素,记忆化将不再起作用。

请查看codesandbox中的上述示例。点击按钮重新渲染,并注意控制台输出。稍微降低 CPU 使用率,即使肉眼也能看出点击按钮时的延迟!


如何限制 CPU 频率

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

为什么将“索引”作为“键”属性不是一个好主意

现在应该很清楚为什么我们需要稳定的“键”属性,以便在重新渲染后仍然有效。但是数组的“索引”呢?即使在官方文档中,也不建议使用索引,理由是它可能会导致错误和性能问题。但是,当我们使用“索引”而不是唯一的值时,究竟发生了什么会导致这些后果呢id

首先,上面的例子中不会出现这些问题。所有这些错误和性能问题只会在“动态”列表中出现——这类列表中,元素的顺序或数量会在重新渲染之间发生变化。为了模拟这种情况,让我们为列表实现排序功能:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

每次点击按钮,数组的顺序都会反转。我将实现两种不同版本的列表,以country.id作为键:

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

并将数组index作为键:

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

Item为了提高性能,直接使用 memoise组件:

const ItemMemo = React.memo(Item);
Enter fullscreen mode Exit fullscreen mode

这里是包含完整实现的codesandbox。在 CPU 受限的情况下点击排序按钮,注意基于“index”的列表速度稍慢,并留意控制台输出:在基于“index”的列表中,每次点击按钮时每个项目都会重新渲染,即使列表Item已被缓存,理论上不应该这样做。基于“id”的实现(除了键值不同之外,其他部分与基于“key”的实现完全相同)则不存在这个问题:点击按钮后不会重新渲染任何项目,控制台输出也干净无误。

为什么会发生这种情况?秘诀当然在于“密钥”值:

  • React 会生成“之前”和“之后”的元素列表,并尝试找出“相同”的元素。
  • 从 React 的角度来看,“相同”的项是指具有相同键的项。
  • 在基于“索引”的实现中,数组中的第一个元素始终为 1 key="0",第二个元素始终为key="1"2,依此类推——无论数组如何排序。

因此,当 React 进行比较时,如果它key="0"在“before”和“after”列表中都看到了同一个元素,它会认为这是同一个元素,只是 props 值不同:country反转数组后,props 值发生了变化。因此,它会像处理同一个元素一样,触发重新渲染周期。由于它认为countryprop 值发生了变化,所以会绕过 memo 函数,直接触发该元素的重新渲染。

图片描述

基于 ID 的行为是正确且高效的:项目能够被准确识别,并且每个项目都会被记忆,因此不会重新渲染任何组件。

图片描述

如果我们给 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

请查看相同的 codesandbox,但这次先点击几个国家/地区,触发背景更改,然后再点击“排序”按钮。

基于 ID 的列表行为完全符合预期。但基于索引的列表现在表现异常:如果我点击列表中的第一个项目,然后点击排序,第一个项目始终保持选中状态,无论排序如何。这正是上述行为的症状:React 认为状态key="0"改变前后(数组中的第一个项目)完全相同,因此它重用了同一个组件实例,保持状态不变(即该项目isActive的状态true为 true),只是更新了 props 值(从第一个国家/地区更新到最后一个国家/地区)。

如果不进行排序,而是在数组开头添加一个元素,也会发生完全相同的情况:React 会认为key="0"第一个元素保持不变,而最后一个元素才是新添加的元素。因此,如果选中第一个元素,在基于索引的列表中,选中状态会停留在第一个元素,所有元素都会重新渲染,并且最后一个元素的“mount”事件会被触发。而在基于 ID 的列表中,只有新添加的元素会被挂载和渲染,其余元素则保持静默。你可以在codesandbox中查看。限制 CPU 性能后,基于索引的列表中添加新元素的延迟会再次变得非常明显!即使 CPU 性能被限制在 6 倍,基于 ID 的列表仍然运行得非常流畅。

为什么将“索引”作为“键”属性是个好主意

读完前面的章节,很容易想到“始终id为‘key’属性使用唯一值”,对吧?大多数情况下确实如此,如果你一直这样做,可能没人会注意到或在意。但当你掌握了相关知识,你就拥有了超能力。现在,既然我们知道了 React 渲染列表时究竟发生了什么,我们就可以“作弊”,用`reduce` 而不是 `reduce`来更快地id创建一些列表indexid

一个典型场景:分页列表。列表中的项目数量有限,点击按钮后,您希望在相同大小的列表中显示相同类型的不同key="id"项目。如果采用传统方法,每次切换页面时都会加载一组全新的项目,每个项目的 ID 都完全不同。这意味着 React 将无法找到任何“已存在”的项目,需要卸载整个列表并重新加载一组全新的项目。但是!如果采用另一种方法key="index",React 会认为新“页面”上的所有项目都已存在,只需用新数据更新这些项目,而无需卸载实际的组件。即使数据集相对较小,如果项目组件比较复杂,这种方法也能明显提高速度。

请查看codesandbox中的这个示例。注意控制台输出——在右侧基于“id”的列表中切换页面时,每个项目都会重新挂载。但在左侧基于“index”的列表中,项目只会重新渲染。速度快得多!即使在 CPU 受限的情况下,一个只有 50 个项目的非常简单的列表(仅包含文本和图像),基于“id”的列表和基于“index”的列表在切换页面时的性能差异也已经非常明显。

对于各种动态列表式数据,情况也完全相同:你需要用新数据集替换现有项,同时保持列表的外观,例如自动完成组件、类似谷歌的搜索页面、分页表格。只需注意在这些项中引入状态即可:它们要么必须是无状态的,要么状态应该与 props 同步。

钥匙都在正确的位置!

今天就到这里!希望你喜欢这篇文章,并且现在对 React 的“key”属性的工作原理、如何正确使用它,甚至如何灵活运用它的规则来提升性能有了更深入的了解。

以下是一些需要牢记的关键要点:

  • 切勿在“key”属性中使用随机值:这会导致每次渲染时项目都会重新挂载。除非这正是您的本意。
  • 在“静态”列表中(即元素数量和顺序保持不变的列表),使用数组索引作为“键”并没有什么坏处。
  • 当列表可以重新排序或可以在随机位置添加项目时,请使用项目唯一标识符(“id”)作为“键”。
  • 对于包含无状态项的动态列表(例如分页列表、搜索结果和自动完成结果等),您可以将数组索引用作“键”,其中项会被新项替换。这将提高列表的性能。

祝您今天过得愉快!愿您的列表项永远不会重新渲染,除非您明确指示它们这样做!✌🏼

...

本文最初发表于https://www.developerway.com。该网站还有更多类似文章 😉

订阅新闻简报在 LinkedIn 上与我们联系在 Twitter 上关注我们,即可在下一篇文章发布时立即收到通知。

文章来源:https://dev.to/adevnadia/react-key-attribute-best-practices-for-performant-lists-2djc