React列表循环为什么需要key | 面试题

3,681 阅读9分钟

大家在开发React应用的时候,都遇到过将数组中的数据渲染成列表的情况,通常的做法是使用map()/filter()等方法,直接将数组转化为React集合,之后插入到指定的位置。在转化的过程中,需要每一个新创建的React组件显示地声明一个key属性

import React from 'react'
export default () => {
  const cities = ['北京', '上海', '广州', '深圳'];
  return (
    <div style={{ width: 500, margin: '0 auto', fontSize: 30 }}>
      <div>当前环境:{process.env.NODE_ENV}</div>
      {
        cities.map((item, index) => {
          // 没有声明key属性
          return <div>{item}</div>
        })
      }
    </div>
  );
}

微信截图_20201223232202.png

我们可以看到如果没有这个key属性,在dev环境下,渲染后虽然页面正常显示,但是React会在控制台打印Warning

image.png

生产环境中却什么都不会提示.

对于很多刚开始接触React的人来说,可能会忽略掉这个 Warning,因为毕竟没有影响到线上任何功能。 但对于React来说,这个key属性真的是一个可选项吗? 为什么要设计这个key属性呢?

接下来我们深入了解一下

  • key属性的作用
  • 如果没有key属性,会带来什么样的问题
  • 如何声明key属性
  • eslint中与key属性有关的规则

React 是一个典型的 UI= fn(state) 的渲染引擎,提供了声明式 API,能够让开发者在开发时,并不用过多的关注页面是如何被渲染的,而只需要控制数据的变化。

image.png

新旧Dom树对比后再转化为真实Dom树

每当有 propsstate 变化时,render方法就会返回一棵 React 组件树,React 会比较数据变更前后所返回的组件树之间的差异,以决定是否需要更新到真实 Dom。

如果一个组件内部异常复杂的时候,新旧组件树之间的对比会极其消耗性能。算法复杂度能达到 n 的三次方。显然,这样的性能开销是无法被接受的

一. key属性作为React性能优化方案之一

React为了解决前面的性能问题,提出了一种启发式算法,将算法复杂度降低到 O(n)。启发式算法的原理是对 React 组件做了假设和预判。

image.png

假设的意思是,如果 React 组件或者 Dom 元素的类型不同,将会渲染出完全不同的树。

image.png

预判的意思是,任意一个父组件的每一次渲染,同一个 key 属性的子组件应该是稳定的(每一次父组件执行 render() 方法的时候,同一个 key 属性值永远标识唯一的 React 组件)。

到目前为止,我们的主角 key属性出现了。key 属性是React用来匹配新旧组件树中子组件的标识,能够让 React 了解哪些组件需要被变更, key 属性能够大大提高启发式算法的性能,并保证 React 正常运行。

现在我们知道key属性的作用了,下面我们深入看一下,如果不声明key属性,会带来哪些性能上的开销。

1.1 不使用key属性的性能问题

image.png

不使用key时会有警告

页面中已经渲染了四座城市,并没有声明key属性,在控制台中 React给出了报错信息

现在我们如果向其中插入第五个城市:杭州。向列表中插入一条数据,分三种情况 • 头部插入 • 中间某个索引位置插入 • 尾部插入

image.png

头部插入,React 将会从顶部开始变更所有组件,现有的组件都不会被保留,会变更插入位置后方的所有组件

中间某个位置插入, 与头部插入类似,会变更索引位置后方的所有组件。

image.png

尾部插入,根据 Reconcliation 机制,性能开销是最小的,因为再次渲染时,React 会根据顺序比较前面的组件,如果与之前的组件匹配,那么就不变更, 只是针对新插入的尾部执行新增操作。

1.2. 使用 key 属性的好处

image.png

使用key后最明显的效果是没有warning

当我们为北上广深对应的四个组件声明了key属性之后,由于React启发式算法中将key属性作为组件唯一标识的规则,无论是从哪个位置插入 新元素, React 在下次渲染的时候,仍然能够识别这些组件,不会对原来的旧组件执行变更操作

image.png

现在我们了解了不声明key属性带来的性能问题,接下来让我们想想如何声明key属性,key属性是随意赋值呢?还是有一些特殊的规范?

二. key 属性的规则

key属性的赋值需要遵循两个原则 • 唯一 • 稳定

2.1 唯一

image.png

key不能重复

React用key来识别组件,当我们用城市的名字为为key属性赋值时,将新插入的城市 有意取名为北京,就会造成key属性的重复,当页面渲染时,控制台会打印警告⚠️信息,提示这么做是有问题的

2.2 稳定性

将可变数据设置为 key 属性的值产生的问题,如随机数

有人看到前面的唯一原则,会想到如果将key属性的值设置为随机数,那么这样所有组件的key属性就一定不会重复了

image.png

使用随机数作为key后没有warning

如果仅仅是一次渲染(在componentDidMoutuseEffect中调用Math.random() 来生成key),方案是可行的,而不是在render()函数中调用 Math.random()

因为React 是在不断重新渲染的,当下一次渲染时,随机数会将为同一个组件生成不同的 key 属性,这样 React 就会认为这是新的组件,而旧的组件已经没有用了,React 就会销毁并卸载旧的组件,之后重新创建组件在内存中的实例对象,接着执行变更 Dom 的操作,这些不必要的操作会带来大量的性能问题,更严重的会导致组件状态的丢失

当没有为 列表中的子 组件声明 key 属性时,勾选某一个 子组件前面的复选框,然后在尾部添加一条新的 数据 这时,checkbox 组件的状态是被保留的,这符合开发预期。这是因为在没有为组件设置 key 属性的情况下,React 会默认将数组索引设置为 key 属性的值

Kapture 2020-12-24 at 14.23.46.gif

把列表子组件 的 key 属性赋值为 Math.random()时, 勾选某一个 子组件 前面的复选框,然后在尾部添加一条新的 数据, 这时,checkbox 组件的状态被清空了

这是因为 key 属性是随机数,每当render函数重新执行时,key属性的值都不同。 React 执行插入的渲染就会销毁之前所有的组件,并创建新的组件,之前组件中保存的状态都会消失

现在我们掌握了声明key属性的两个原则,接下来看看在实际的开发中,我们应该如何声明key属性呢?

将数组索引设置 key 属性的值的问题

可能有同学会想到,既然组件集合是由数组生成,那么我们是不是可以把数组索引当做key属性的值。

数组索引是从0 开始的自增序列,所以天然具有唯一性,并且React每次渲染后,索引都不会改变,也符合稳定性的要求!

接下来我们看看用数组索引作为key属性的值,是否还出现复选框状态丢失的问题

Kapture 2020-12-24 at 14.42.55.gif

当为子组件的key属性的值改为数组索引后,复选框状态不会出现丢失的情况

当我们使用数组索引为key属性赋值后,一切都表现的很正常和完美,那么是不是这就是最佳实践呢?显然不是的。如果数组索引是最佳实践,那么React可以在其内部渲染时,就为每一个组件自动分配数组索引作为key属性的值,而不需要我们手动赋值。实际上在没有为组件声明key属性的情况下,这确实也是React的默认行为。

现在我们为列表增加一个排序功能,排序很简单,就是将 "北上广深" 倒序输出。让我们看看接下来会发生什么❓

Kapture 2020-12-24 at 17.35.46.gif

  1. 勾选 "北京","上海" 前面的复选框
  2. 任意添加一条数据
  3. 点击倒序排列按钮

这时所有的数据都会倒序排列,但"北京","上海" 前面的复选框状态丢失了,而排列后的前两条数据的复选框被勾选了,问题出现了:复选框的状态没有跟随数据的变化而更改

原因:当第一次渲染时,子组件 列表的 key 属性被赋值为数组索引, 如果仅仅在尾部插入一个新的组件,前面组件的索引值并不会被变化, 但是,对数据进行了重新排序,数组索引 index 仍然稳定地从 0 开始自增, React 认为组件并没有发生变更。

image.png 因此 对应索引的 checkbox 仍然被选中了, 但实际上, React 组件的内容已经发生了变化。

那我们到底该如何声明key属性的值呢?

React官方给出的建议是key属性的值 应该是字符串类型的唯一标识,通常这个唯一标识来自于API返回给前端的数据。后端返回的数据中通常都有一个id这类的字段,这个字段一般都是全局唯一,并且是稳定地保存在持久层中的

a.gif

我们为数组中每个元素声明两个字段,code和name.然后将code作为key属性的值。我们再次对数据进行添加和倒叙排列,"北京"和"上海"的复选框状态没有被丢失。

那么数组索引是不是一定不能作为key属性的值呢?其实也是有特例情况的,如果满足接下来三个条件,那么也是可以的。

将数组索引设置为 key 属性的值的前提条件

• 数据和组件都是静态的,首次渲染后不变化 • API 返回的数据中没有唯一 id • 列表不会被重排或筛选

使用场景, 如法律条款列表, 注意事项列表....就比如我们上面的代码。

三.eslint 插件帮助更好的申明 key 属性

eslint-plugin-React 中的React/jsx-key, no-array-index-key, 分别用来检查是否声明了 key 属性, 是否使用了数组索引声明 key 属性

参考文档

  1. 为什么React组件要声明key?-geek
  2. react-key官方文档