为什么 React 渲染列表时需要加上 key?
2022年2月15日
当我们在 React 要渲染一个列表时,如果没有在每一个被渲染的元件加上 key 这个 prop,就会跳出 Warning: Each child in a list should have a unique “key” prop.
这个错误讯息。直到开发者把 key
加上后,这个警告讯息才会消失。为什么要加上 key? 以及 key 有什么原则需要遵守? 这是在开发 React 时需要有的重要概念,也是面试经常会问的基础题。
为什么需要 key?
key 就像一个独特身份,让 React 可以去分辨哪些子元件被新增、 移除,或是修改。 (编按:若要进一步说明此概念,推荐在面试时画下 Dan Abramov 的这个系列推文例子)
从上面的例子可以看到,当今天红黄蓝三个圈,变成红蓝黄;这会有两个可能性。可能性一第二个圈跟第三个圈的位置互换;可能性二是位置没互换,但第二个圈变蓝色、第三个圈变黄色。如果没有一个独特辨识的机制,我们会没办法知道,究竟是哪一个可能性。
如果没办法有效辨识,将可能出现 bug。举例来说,假如今天的圈圈是有状态的,例如里面有勾选方块,然后第个二圈有被勾选。假如今天换颜色是因为第二个圈被交换到第三个位置,这时表示新一次的渲染时,第三个圈要是被勾选的。不过假如我们没有一个辨识机制,要是 React 误以为换颜色不是因为换位置,而是单纯的第二个圈换颜色,那么将会渲染出仍是第二个圈是被勾选的;那么这就会是一个 bug。
然而,有了 key 这个辨识机制,React 就会知道在新的一次渲染时,原本的状态应该被保留在列表中的哪一个元件中。因此,React 之所以需要 key,正是因为 key 可以让 React 知道,哪些子元件被新增、 移除,或是修改。
除此之外,看到下面这段 React 官方文件的例子 (编按:面试时也推荐直接举这例子)。原本有个列表,我们在最上方新增一个 <li>Connecticut</li>
,如果没有 key,对 React 来说将会是,Duke 变成 Connecticut、Villanova 变成 Duke,而最后新增一个 Villanova。换句话说,整个列表都改变了,所以 React 会打掉旧的,重建一个新的列表。当列表变大时,就会很消耗效能。
// 原本
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
// 改变后
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
然而,如果有了 key。 React 透过 key 发现 2015 与 2016 都没有变,所以不动他们。这时只会在最上面多加上
<li key="2014">Connecticut</li>
这比起打掉整个列表再重建,会有效率非常多。
// 原本
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
// 改变后
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
总结来说,key 可以让 React 知道,哪些子元件被新增、 移除,或是修改。这一来可以避免一些跟状态改变有关的 bug,二来也可以让 React 在更新时的渲染更有效率。
追问题:使用 key 有哪些原则要遵守?
key 需要是独特唯一 (unique)
如上面提到 key 是让 React 用来辨认列表中的子元件的机制,因此 key 需要是独特唯一,不然会有潜在的 bug 的可能性。
key 需要稳定且可预测,不然效能会变差
举例来说,不能用 key= {Math.random()}
。之所以不能用随机值,是因为 React 的渲染机制与背后的演算法,会再遇到key
时,去比对原本的节点,如果发现 key 不一样,React 就会把旧的节点销毁,然后创建一个新的节点。如果该节点根本没有改变,只因为 key 不同就被打掉重建,这将会造成不必要的重建,也会让效能变差(编按:有兴趣多了解可读这篇 React 的官方文件)。
避免把 index 当成 key
事实上,如果没有特别去设定 key,React 预设会把列表的 index 当成 key。如果这个列表都不会有顺序上的改变,例如上面的三个圈圈不会有位置交换,只有圈圈本身的颜色改变,那不会有什么问题。只是如果有位置的改变,把 index 当成 key 就可能产生 bug。
举例来说,今天如果有下面这样的列表,我们在 index 为 1 的地方加入一个项目,这时会导致原本 index 为 1 的变成 2。如果 index 为 1 的本来有一些状态,例如原本 Duke 有被勾选,这时会变成 Boston 被勾选,但是 Duke 反而没被勾选。这是我们要避免的 bug!
// 插入新元件前
<ul>
<li key="0">Connecticut</li>
<li key="1">Duke</li>
<li key="2">Villanova</li>
</ul>
// 插入新元件后
<ul>
<li key="0">Connecticut</li>
<li key="1">Boston</li>
<li key="2">Duke</li>
<li key="3">Villanova</li>
</ul>
总结来说,key 需要是独特唯一、key 需要是稳定可预测,以及避免用 index 作为 key。这三点是我们在使用 key 时需要避免的。