likes
comments
collection
share

「好文翻译」React key属性:高性能列表的最佳实践

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

译者的话

本文依然翻译自外网博客 DeveloperWay: React key attribute: best practices for performant lists

原作者:NADIA MAKAREVICH

「好文翻译」React key属性:高性能列表的最佳实践

这篇文章讲的是 React 中的 key。一个非常基础,但又总是让人“略懂”的属性。不少人知道应该使用具有唯一性的 id 给它赋值,但对内部的工作细节多少还有些模模糊糊。如果你和译者一样,也在“略懂”之列,那可以看看这篇文章,它会让你的理解变得更清晰。如果你已经是 React 高手,那大可以关掉了,这篇文章对你多少有些新手向了。

写在前头

React 的 key 属性可能是 React 中使用最多的 “自动驾驶 ”功能之一😅。我们当中有谁能诚实地说,我们他们使用它是因为 “...某些合理的原因”,而不是因为 “eslint rule 正在飙红”呢?

我怀疑大多数人在面对 「为什么 React 需要  key 属性 」这个问题时,会回答 “呃......我们应该把唯一值放在那里,这样 React 才能识别列表项,这样对性能更好”。从技术上讲,这个答案是正确的。 但是 「识别列表项」 到底是什么意思呢?如果我跳过 key 属性会发生什么?应用程序会崩溃吗?如果我在这里输入一个随机字符串会怎样?值的唯一性如何?我可以使用数组的索引值吗?这些选择有什么影响?它们对性能有什么影响? 让我们一起来研究一下吧! 

React key 属性是如何工作的

首先,在开始编码之前,让我们先搞清楚理论只是:key 属性是什么,为什么 React 需要它。 简而言之,如果 key 属性存在,React就会在重渲染时使用它来识别同类型元素(参见官方文档:reactjs.org/docs/lists-…reactjs.org/docs/reconc…

换句话说,只有在重渲染时为了和相邻的同类型元素(即扁平列表)区分时,才需要 key 属性(这一点很重要!)。 

重渲染过程的简化算法如下:

首先,React 将生成元素 「之前」和 「之后」的 快照。

其次,React 将尝试识别页面中已经存在的元素,以便重新使用它们,而不是从头开始创建。

  • 如果 key 属性存在,它将假定重渲染 「之前」和「之后」key 相同的项是相同的。
  • 如果 key 属性不存在,它将使用索引作为默认 key

第三,它会:

  • 删除在重渲染「之前」存在但「之后」不存在的项目——即卸载(unmount)它们。
  • 从头开始创建在「之前」阶段不存在的项目——即加载(mount)它们。
  • 更新 「之前」存在并在 「之后」继续存在的项目——即重新渲染(rerender)它们 。

下面让我们看看代码。

为什么说随机 key 是糟糕的实践?

让我们先实现一个国家列表。我们将有一个 Item 组件,渲染国家信息:

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

同时, CountriesList 容器组件来渲染真正的列表:

const CountriesList = ({ countries }) => {
  return (
    <div>
      {countries.map((country) => (
        <Item country={country} />
      ))}
    </div>
  );
};
  • React会发现这里没有 key,于是便会退回到使用国家数组的索引作为key。
  • 我们的数组没有改变,所以所有项目都将被识别为 "已经存在",列表项将重渲染。

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

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

「好文翻译」React key属性:高性能列表的最佳实践

现在有趣的部分来了:如果我们不使用索引,而是在 key属性中填充一些随机字符串呢?

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

在这种情况下:

  • 在每次重新渲染 CountriesList 时,React 将重新生成 key 属性。
  • 由于key属性是存在的,React 将使用它作为识别「现有」元素的一种方式。
  • 由于所有的 key属性都是新的,所有 "之前" 的项都将被视为 "已移除",每个项都将被视为 "新的",所以 React 将unmount所有列表项并将它们重新mount回去。

「好文翻译」React key属性:高性能列表的最佳实践

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

当我们谈论性能时,与简单的 re-render 相比,组件的重新挂载要昂贵得多。此外,用React.memo包装Item所带来的所有性能提升都将消失——缓存将不起作用,因为Item将在每次重新渲染时重新创建。

看看codesandbox中的上述示例。点击按钮重新渲染,注意控制台输出。稍微节流一下CPU,点击按钮时的延迟甚至可以用肉眼看到!

如何控制 CPU 节流(throttle)

在Chrome浏览器的开发工具中打开 "性能 "选项卡,点击右上角的 "齿轮 "图标——它将打开一个额外的面板,"CPU节流 "是其中的一个选项。

为什么把索引作为 key 也不是好主意?

现在,我们应该很清楚为什么我们需要在重渲染时稳定不变的 "key "属性了。但是数组的 "索引"呢?官方文档中并不推荐使用它们,理由是它们可能会导致错误和影响性能。但是当我们使用索引而不是某个唯一的 id 时,到底发生了什么会导致糟糕的后果呢?

首先,我们不会在上面的示例中看到这些问题。所有这些错误和对性能的影响只发生在 "动态"列表中——在列表中,项的顺序或数量在重新呈现时会发生变化。为了模仿这种情况,让我们为列表实现排序功能:

const CountriesList = ({ countries }) => {
  // 引入排序值
  const [sort, setSort] = useState('asc');

  // 根据 state 的值,使用 lodash 的 orderBy 方法来为国家排序
  const sortedCountries = orderBy(countries, 'name', sort);

  // 加一个按钮,让 state 的值在升序降序间切换
  const button = <button onClick={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>
      toggle sorting: {sort}
  </button>;

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

每当我点击按钮,数组的顺序就会颠倒。我将分别以 id 和 索引作为 key 实现列表。

country.id作为 key

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

以索引作为 key

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

为了性能目的,我们将对Item组件进行缓存:

const ItemMemo = React.memo(Item);

下面是完整实现的codesandbox。在 CPU 节流的情况下点击排序按钮,注意基于索引的列表速度稍慢,并注意控制台输出:在基于索引的列表中,尽管Item已缓存,每个列表项在每次点击按钮时也都会重新显示(虽然在技术上不该如此)。而基于 id 的实现除了 key 值外,与基于索引的列表完全相同。它就没有这些问题:在点击按钮后,没有列表项被重渲染,控制台输出也是干净的。

为什么会出现这种情况?秘密当然在于 key 值:

  • React 创建了「之前」和「之后」的元素列表,并尝试辨认两者之间「相同」的列表项。
  • 在 React 的视角看来,「相同」的列表项指的是在前后过程中有相同 key 的项。
  • 在基于索引的实现中,数组的第一项永远拥有key="0",第二项永远满足key="1",以此类推。这种情况根本和排序与否毫无关系。

所以,当 React 在做比较的时候发现「之前」和「之后」都有key="0"的列表项,就会认为它们是完全「相同」的,只是 props 值有所变化:country 的值在排序后全部变化了。 因此, React 就做了它该做的事:触发重渲染流程。而且,既然它认为 country 值变化了,memo 方法也自然被忽略掉了,每个列表项都将重渲染。

「好文翻译」React key属性:高性能列表的最佳实践

基于 id 的列表的行为就很正确且性能良好:列表项被正确地识别了,每一项都被正确地缓存了,没有一个组件被重渲染(仅仅对两个已经渲染的组件交换了顺序)。

「好文翻译」React key属性:高性能列表的最佳实践

如果我们为Item组件引入一些状态,这种行为就会特别明显。例如,让我们在点击时改变它的背景:

const Item = ({ country }) => {
  // 用于指征国家是否处于 active 状态
  const [isActive, setIsActive] = useState(false);


  // 点击按钮后切换 active 状态
  return (
    <button className={`country-item ${isActive ? 'active' : ''}`} onClick={() => setIsActive(!isActive)}>
      <img src={country.flagUrl} />
      {country.name}
    </button>
  );
};

看看同样的codesandbox,只是这次先点击几个国家,触发背景变化,然后才点击 "排序 "按钮。

基于 id 的列表与您预期的完全一致。但是基于索引的列表现在的表现很奇怪:如果我点击列表中的第一个项目,然后点击排序——不管排序如何,第一个项目仍然被选中。这就是上述行为的症状: React 认为key="0"项目(数组中的第一个项目)在状态改变前后是完全一样的,所以它重新使用同一个组件实例,保持原来的状态(即这个项目的isActive设置为true),只是更新了 props 值(从第一个国家到最后一个国家)。

如果我们不进行排序,而是在数组的开头添加一个项目,也会发生完全相同的事情: React会认为key="0 "的项目(第一个项目)保持不变,而最后一个项目是新的。因此,如果第一个项被选中,在基于索引的列表中,选择将停留在第一个项,每个项都将重新渲染,最后一个项甚至会触发 mount。而在基于 id 的列表中,只有新添加的项目才会被挂载和渲染,其余的项目将静静地放置在那里。在 codesandbox 中查看。调节您的 CPU,在基于索引的列表中,添加一个新项的延迟明显到依旧肉眼可见!而即使在 6 倍 CPU 节流的情况下,基于 id 的列表速度也非常快。

为什么把索引作为 key 又「是」个好主意?

经过前面几节的介绍,我们很容易就会说 “只要在 "key "属性中使用唯一的项目id就可以了”,不是吗?在大多数情况下这是对的,如果你一直使用 id,可能没有人会注意或介意。但是芝士就是力量!现在,既然我们知道了 React 渲染列表时到底发生了什么,我们就可以用 index 代替 id,让一些列表变得更快。

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

请看codesandbox中的示例。注意控制台输出——当您在右侧基于 id 的列表中切换页面时,每个项目都会被重新挂载。但是在左边的基于索引的列表中,项目只是被重新渲染。速度更快!在 CPU 节流的情况下,即使是 50 个非常简单的列表(只有一个文本和一个图片),在基于 id 的列表和基于索引的列表中,切换页面的区别也已经非常明显。

同样的情况也会出现在各种类似于动态列表的数据中,在保留列表外观的同时用新的数据集替换现有的项目:自动完成组件,类似于 google 的搜索页面,分页的表格。只是需要注意在这些项中引入状态:它们必须是无状态的,或者状态应该与 props 同步。

(译者注:即在高频全量变化(翻页)、item 数少且 item 复杂的场景下,使用基于索引的列表将能利用索引的不变性触发重渲染,避免高频卸载挂载,性能反优于 id。对此观点译者持保留意见。)

所有的 Keys 都各归其位!

今天就到这里!希望您喜欢这篇文章,并能更好地理解 React 中 key 属性是如何工作的,如何正确地使用它,甚至如何按照您的意愿修改它的规则,以及如何在性能游戏中玩小把戏。 以下是一些重要启示:

  • 切勿在 key 属性中使用随机值:这将导致项目在每次渲染时重新挂载。当然,除非这就是您的意图。
  • 在「静态」列表(即那些项的数量和顺序保持不变的列表)中使用数组的索引作为 key 是没有坏处的。
  • 当列表可以重新排序或者可以在随机位置添加条目时,使用条目唯一标识符(id)作为 key
  • 对于无状态项的动态列表(例如分页列表,搜索和自动完成结果等),可以使用数组的索引作为 key,在这些列表中,项会被新的项替换(而非卸载后再创建)。这将提高列表的性能。

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

另外,请查看YouTube视频,它使用一些漂亮的动画来解释文章内容,如果你能科学上网的话。

转载自:https://juejin.cn/post/7257022428194521145
评论
请登录