大型揭秘!为什么在JSX数组map时必须传入key!
在 JSX 中,当你创建一个元素列表时,React 要求每个元素都应该有一个唯一的 key 属性。这是为了帮助 React 识别哪些元素改变了、添加了或者移除了,从而提高渲染效率和维护组件状态的稳定性。
问题引入
咱们先来看看一段代码,和它的报错信息
import { useState,useEffect } from 'react'
import './App.css'
function App() {
const [todos, setTodos] = useState([
{
id:1,
title:'标题一'
},
{
id:2,
title:'标题二'
},
{
id:3,
title:'标题三'
}
])
return (
<ul>
{
todos.map((todo)=>(
<li>{todo.title}</li>
))
}
</ul>
)
}
export default App
为什么报错,这个错误是什么意思?
- 咱们先直译一下:
直译:
- 错误信息: 每个列表中的子元素都应该有一个唯一的 "key" 属性。
- 位置: 报错出现在
hook.js文件的第608行。- 建议: 检查
App组件的渲染方法。更多详情请参阅 react.dev/link/warnin…。
相信各位大佬看直译就能看懂了,所以错误的原因就在于我们没有给列表设置唯一的key属性,解决这个问题简单,咱们直接上代码!
todos.map((todo)=>(
<li key={todo.id}>{todo.title}</li>
))
id是我们主动给todos设置的独一无二的属性,刚刚好符合key的要求
但是,但是,在座的各位,就不想知道为什么吗,为什么我就一定要设置这个key,这个key有那么重要吗?那现在就让我们去探索一下,到底为什么!
探索谜题
废话不多说,上代码!
useEffect(()=>{
setTodos(prev=>prev.map(todo => {
if(todo.id ===1) return{
...todo,
title:'标题一更新'
}
return todo
}))
},[])
来个小插曲:
提问:为什么要使用useEffect?直接setTodos不可以吗?
使用
useEffect并结合空依赖数组[]是一种明确告诉 React 我们希望这段代码只在组件挂载时运行一次的方式。这样做可以确保我们的副作用(这里是更新todos)不会被无谓地反复执行,从而优化了组件的行为和性能。而直接调用setTodos则不具备这样的控制能力,容易造成不必要的状态更新和渲染。因此,在需要针对特定生命周期事件执行副作用时,推荐使用useEffect。 其他代码不变,在return之前加上上面这段代码,只改变了第一项其他的没有变
回到正题:
我们都知道setTodos必然会触发组件渲染,生成虚拟DOM,如果DOM发生结构或者样式的改变,将会触发重绘重排,重绘重排非常耗能!
而reat这样一个强大的前端开发框架,将性能优化做到了极致,根据我的直觉,想必谜底就跟虚拟DOM的改变有关系
接下来就让我们专注于看在我运行代码时,到底哪些部分发生了改变!
当然还是报错,但是通过看后台元素的高光,我们会发现只有我们改变了值的那个li进行了改变,其他li元素没有发生变化!
奇怪,我们并没有加上独一无二的key,但是似乎已经做到了性能优化,真的是这样吗?
不要着急,我们继续往下看!
useEffect(()=>{
setTimeout(()=>{
setTodos(prev=>[
{
id:4,
title:'标题四'
},
...prev
])
},5000)
},[])
提问:prev是什么,在这里有什么用?
prev表示 当前的状态值,也就是todos的当前值这是因为:
- React 的状态更新可能是异步的。
- 多个状态更新可能会被批量处理。
- 如果你直接使用
todos(比如setTodos([newItem, ...todos])),可能读取的是旧的、过时的值。使用函数式更新(传入
prev)可以确保你拿到的是最新的状态值。
我们在return之前加入这段代码,实现了在我们所有li之前插入第四个li
诶呀,全部都改变了,按道理来说,应该是只需要改变我们添加的那个li,但是这里全部改变了,耗费了大量性能,看来谜底已经浮出水面了
揭秘时刻
首先我们要理解react底层更新DOM的原理
1.React 的虚拟 DOM Diff 算法原理
我们的内存中存储着新旧两个状态,形成新旧两棵虚拟 DOM 树,React 在更新界面之前,会进行一个 diff 过程:将新旧两棵虚拟 DOM 树进行比较, diff 差值,让界面更新,每一项每一项进行比较找出最小的差异,从而决定如何高效地更新真实 DOM。
- 当你在渲染一个列表(如
map(todos => <TodoItem />))时,React 会为每个元素创建一个虚拟节点。 - 如果没有设置
key,React 默认使用索引 index 作为 key。
这下我们就可以知道了,我们上面代码出现大量li更新的原因是因为我们将第四个li插入到了数组的最前端,导致整个数组所有li的索引都发生了改变,而diff的比较是每一项每一项进行比较,对于没有设置key的li,通过索引来判断是否为同一项,进行比较,不同则改变,此时索引发生了大幅度改变,发生了错位比较,对于v8引擎来说,li的每一项都发生了改变,因此,发生了大量不需要的改变
2. 不设置 key 或使用 index 作为 key 的问题
-
问题一:无法准确识别元素身份
- 如果你使用
index作为key,当数组中的元素顺序发生变化时(比如插入、删除或排序),每个元素的index都会变化。 - React 会认为这些元素是全新的或被修改的,从而触发不必要的重新渲染或重新创建 DOM 节点,造成性能浪费。
- 如果你使用
-
问题二:大量不必要的更新
- 比如你在数组开头插入一个新元素,所有后续元素的
index都会变化。 - React 会误以为这些元素都发生了变化,于是重新创建或更新这些元素,即使它们的内容没有变。
- 比如你在数组开头插入一个新元素,所有后续元素的
3.为什么 key 必须唯一?
-
每个
key应该是一个稳定、唯一且可预测的标识符,比如数据库中的id。 -
这样 React 才能准确地识别每个元素的身份,即使它们的顺序发生变化。
-
使用唯一
key可以确保:- 相同内容的元素不会被错误地重新创建。
- 状态(如输入框内容、组件内部状态)不会因为顺序变化而丢失。
结尾
我们来看看在学习过程中常写的代码
todos.map((todos,index)=>(
<li key={todos.index}>{todos.title}</li>
))
再回过头来看,还真是写了跟没写一样,因为系统默认会将数组的index作为key值,我们没有必要再写一次
好了,就这样吧