凭什么随意设置key会引入React BUG?
2023-12-12 10:04:04
对于React初学者来说,相信 key
的使用应该算是较早接触到的一项知识了,它能快速让开发者意识到在React中,组件的更新是基于比对而进行的,差异更新可以提升性能。
这里涉及到React中Virtual DOM的概念,在React中渲染页面时,并不会直接通过DOM操作进行,而是先将数据通过JSX语法或
createElement
方法抽象成一个Virtual DOM,而key
的作用就是帮助Virtual DOM在更新时区分出那些没有变化的元素,从而省去这些元素在真实 DOM 上的更新操作。
不过,说到React初学者对于 key
的认识,可能就停留在仅知道它能保证列表的正常渲染上,往往对于 key
的具体值并没有什么要求,只要随便设置一个不重复的值,或者使用数组的下标作为值,应该就可以正常使用了。
const App = () => {
const list = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
{ id: 3, name: "王五" },
];
return (
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};
这时可能就有读者会问了,我的
key
是根据唯一的主键(如id
)设置的,为什么还会出现这样的错误呢?
前面我们说到,key
只是在Virtual DOM更新时,用以比对以确定那些没有发生变化的元素,从而可以省去那些不需要更新的元素在真实 DOM 上的更新操作,以提升渲染性能。
因此,只要在Virtual DOM更新时,Virtual DOM中每个元素的
key
是唯一且和真实DOM上元素的key
保持一致的,那么Virtual DOM的更新就可以进行,也就不会出现错误了。
不过,由于在绝大多数情况下,我们在React应用中对Virtual DOM的更新,都是通过setState这种状态更新所触发的,而状态更新所对应的Virtual DOM更新,在绝大多数情况下,都是所有Virtual DOM节点树的更新。
这样一来,只要某个Virtual DOM节点树内的
key
出现了重复,那么在Virtual DOM更新时,就有可能出现多个拥有相同key
的元素。根据前面所说,如果Virtual DOM中每个元素的key
是唯一且和真实 DOM 上元素的key
保持一致的,那么 Virtual DOM 的更新就可以进行。
所以,只要在某一棵Virtual DOM节点树的更新时,其内部出现了 key
重复的元素,那么Virtual DOM就无法更新,直接报错:
Invalid hook call. Hooks can only be called inside of the body of a function component.
此处以一个案例来说明这个问题:
const App = () => {
const [count, setCount] = useState(0);
const list = [];
for (let i = 0; i < count; i++) {
list.push({ id: i, name: `姓名${i}` });
}
return (
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};
-
上述代码中,我们用一个循环生成了一个长度为
count
的数组,数组中每个元素是一个对象,对象有id
和name
两个属性,其中id
是唯一且连续的,而name
则是姓名
加上id
的字符串。 -
然后我们用这个数组,通过
map
方法生成了一个li
标签的数组,每个li
标签的key
都等于对应的对象id
属性,然后将这个数组通过ul
标签渲染了出来。 -
此时,这个组件的初始渲染没有任何问题,因为根据上面的分析,Virtual DOM中每个元素的
key
是唯一且和真实 DOM 上元素的key
保持一致的。 -
不过,当我们点击按钮,调用
setCount
方法时,会将count
的值加 1,那么在组件更新时,Virtual DOM 就需要更新。此时,Virtual DOM 中每个元素的key
不再唯一了,因为在创建数组时,为了保证循环的连续性,新添加的数组元素的id
恰好等于旧数组的最后一个元素的id
,导致key
出现了重复。
因此,在Virtual DOM更新时,就会出现重复的
key
,导致报错。
最后,想要避免这个问题,我们只需要保证Virtual DOM中每个元素的 key
是唯一且和真实 DOM 上元素的 key
保持一致即可,一般我们使用对象的主键(如上面例子中list
数组元素的id
属性)作为 key
是不会出错的。
若实在是无法保证唯一性,那么也可以使用像 uuid
这样的第三方库来生成唯一的 key
。