聊聊React中的key

3,886 阅读6分钟

前前言

朋友们好啊,我是来自推啊前端团队的 zhouchao 同学,本次分享的内容是「聊聊React中的key」。如果大家有不同的观点,欢迎在评论区吐槽和指正哦~😝😝😝

为什么要有 key

参考react官方文档的说法,在递归一个数组时,如果没有其他技术的接入,会根据前后两棵树,从前往后的进行1对1的比较,没有任何复用的逻辑。当我们遇到是在数组最前面增加一项时,react会认为每一项都发生了变化,使得每一项都需要进行更新,这种不合理的更新方式迟早会导致性能问题😵。

而在对数组中的每一项都增加 key 之后,react就会在前后两棵树中,先去找有没有相同的key来做复用。当我们能够给到react合适的key,react就可以合理的判断出,到底是哪里需要增加或者删除。

reactjs.org/docs/reconc…

key 该如何指定呢

前面介绍了key作为react引入的一种用于定义组件是否变化的属性,那我们开发者作为使用方,该如何去指定key来达到优化性能的要求呢?

  1. 找到数组内,对每一条数据来讲都是唯一的那个属性,比如说在渲染一个服务端返回去的列表时,可以用id来作为key;如果服务端不提供,我们也可以根据实际的情况,自己去设计用某些拼接起来来表示key。

  2. 最轻松的,用数组的索引来做key。

看到这两种指定key的方式后,有react基础的同学立马就能意识到,第一种是最好的,因为第二种会有问题,不推荐用,balabala......

没错,用数组的索引来做key确实会存在问题,今天就带大家一起来探讨下,到底是在怎样的场景下,用数组的索引做key会出现问题

索引做 key 是100%错误的吗🧐?

该不该一棒子把索引做 key 打死呢?我们用几个实际场景下的例子,来体会下到底是不是这样。

export default function App() {
  const [arr, setArr] = useState(
    Array(5)
      .fill("")
      .map((i, idx) => ({ id: idx, value: idx }))
  );

  const remove = useCallback(
    (index) => {
      setArr(arr.filter((i, idx) => idx !== index));
    },
    [arr]
  );

  const change = useCallback(
    (e, index) => {
      setArr(
        arr.map((i, idx) => {
          if (idx === index) {
            return {
              ...i,
              value: e.target.value
            };
          }
          return i;
        })
      );
    },
    [arr]
  );

  return (
    <div className="App">
      {arr.map((item, index) => {
        return (
          <div key={index}>
            <span style={{ marginRight: 10 }}>id:{item.id}</span>
            <span>输入框:</span>
            <Input
              value={item.value}
              onChange={(e) => change(e, index)}
            />
            <DeleteOutlined onClick={() => remove(index)} />
          </div>
        );
      })}
    </div>
  );
}
复制代码

在👆这个例子中,当我们在第1项和第三项的输入框中增加内容后,删除掉第2项,大家可以试想下界面上应该会有哪些变化?

  1. 是第二行的id和输入框被完全删掉,然后下面几行上移;
  2. 还是第二行的id被删掉了,但输入框保持了之前的内容,反而是最后一行的输入框被删掉了;从第2行开始到最后一行,输入框的内容和之前的都不一致;

duang,实际结果是!!!

再换个例子,这个例子和之前的区别就在于,我们把Input组件从受控换成了非受控。

export default function App() {
  const [arr, setArr] = useState(
    Array(5)
      .fill("")
      .map((i, idx) => ({ id: idx, value: idx }))
  );

  const remove = useCallback(
    (index) => {
      setArr(arr.filter((i, idx) => idx !== index));
    },
    [arr]
  );

  const change = useCallback(
    (e, index) => {
      setArr(
        arr.map((i, idx) => {
          if (idx === index) {
            return {
              ...i,
              value: e.target.value
            };
          }
          return i;
        })
      );
    },
    [arr]
  );

  return (
    <div className="App">
      {arr.map((item, index) => {
        return (
          <div key={index}>
            <span style={{ marginRight: 10 }}>id:{item.id}</span>
            <span>输入框:</span>
            <Input
              defaultValue={item.value}
              onChange={(e) => change(e, index)}
            />
            <DeleteOutlined onClick={() => remove(index)} />
          </div>
        );
      })}
    </div>
  );
}
复制代码

在Input组价为非受控的情况下,重复之前的步骤,又会是怎样的结果呢?大家可以思考一下

受控&非受控 是问题的关键

在前后两个例子中,我们都用了索引做 key ,左侧的id一直是能够根据实际情况进行更新的。而在Input组件这里,受控状态下的Input组件,是能够正确更新的,而在非受控状态下的Input组件,从被删掉的位置开始,后续的所有值都发生了错乱。接下来我们从react如何进行更新的角度来分析,为什么会出现这样的差异。

reconciliation

了解react的同学都知道,在数据发生变化时,react会进行dom diff,找出最小的改动再应用到真实的dom上。在通过遍历数组再生成节点的场景中,key 在其中发挥了很重要的作用。

在第一个🌰中,数组从5项变为了4项,也就是在react的虚拟dom中,原来的5项,分别由 key 为0、1、2、3、4表示,新的4项,分别由 key 为0、1、2、3表示。react发现key 等于4这项没有了,就把它删除掉,其余的4项都可以复用,从而重新调用这4项的渲染方法(在本例中就是重新渲染map函数中的返回值)。又由于此时的Input为受控模式,渲染内容完全受数据所控制,所以渲染结果正常。

再来到第二个🌰,在react角度来看,key的改动是一致的,都是删除掉最后一项,然后将前面的4项进行更新。但现在问题来了,id:{{item.id}}确实是按照拿到的id进行正常的渲染,但是Input组件此时是非受控模式(数据的变动不会反映在UI上)。

我们以id=3的这条数据为例,它在初次渲染时的key是3,删掉第二条数据后,它的key变成了2。react会复用key=2这个节点并会用{id:3,value:3}进行重新渲染,但key=2这个节点,它之前的Input中显示的是22222并且由于它是非受控模式,它并不会因为数据的变化导致UI的变化,所以我们看到的还是22222,最终就导致我们看到了 id:3 输入框:22222 这样出乎我们意料的结果。

那么,用唯一的一个值做 key ,又是怎么做到不出问题的呢?

如果我们用id作为key,在修改第一项和第三项的Input后再删除第二项,此时react对比新旧dom树时,key由原来的0、1、2、3、4变成了0、2、3、4,react清晰地知道是key=1这一项被删除了,所以很准确地删掉了它,不需要修改其他节点,最终页面正确显示。

小结一下:

造成这种UI错乱的原因有:

  1. 用了索引做key,导致react的更新行为,与我们实际操作的不一致(我们的主观意愿上是想删掉id=1这个组件,并不希望其他的组件发生任何变化)
  2. 在用新的数据进行渲染的过程中,存在一部分内容,它的UI展示不受数据所影响

总结

如果我们要渲染的内容是完全受数据所控制的,那用索引做 key 也是没问题的;但如果做不到这一点,就会出现我们意想不到的结果;

作者:zhou_chao
链接:juejin.cn/post/698986…
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。