key 是 React 用于识别列表中哪些子元素被改变、添加或删除的唯一标识符
它帮助 React 更高效、更准确地更新和重新渲染列表
1、核心原因:Diff算法与性能优化
React 的核心思想之一是通过虚拟 DOM (Virtual DOM) 来减少对真实 DOM 的直接操作,从而提升性能。当组件的状态或属性发生变化时,React 会创建一个新的虚拟 DOM 树,并将其与之前的虚拟 DOM 树进行比较。这个过程叫做 “Diffing”(差异比对)
假设:在处理列表时候,没有key(React的diffing 算法会变更很低效),默认索引为key作为标识,
原本有一个列表[‘A’,‘B’,‘C’,‘D’],现在需要再原来的基础上在开头新增一个元素 变为 [‘E’,‘A’,‘B’,‘C’,‘D’]
如没有key:
React 会比较第一个位置的 A 和 X,发现内容变了,于是更新第一个
- 的文本。接着比较第二个位置的 B 和 A,又更新…以此类推。它会修改所有已有的子元素,而不是意识到 A, B, C, D 只是位置后移了,最终还会在末尾创建一个新的
- 。这导致了大量不必要的真实 DOM 操作(更新),性能极差
-
有key时:
React 通过 key(例如 key={item.id})知道 A, B, C ,D 仍然是原来的项,只是移动了位置。它只会创建一个新的
- (对应 X)并插入到开头,然后移动原有的三个
- 。这个过程(移动和插入)远比更新每个节点的内容要高效。
-
2、保持状态的正确性(这是更重要的原因!)
key 不仅仅是性能优化,它直接关系到组件实例状态的正确性。React 使用 key 来匹配多次渲染间的组件实例。如果 key 保持不变,React 会认为这是同一个组件实例,因此它会保留该组件的状态(如 useState、useRef 的值)。如果 key 改变了,React 会销毁旧的组件实例并创建一个新的
看一个经典的例子:一个待办事项列表,每个事项有一个输入框可以编辑。
// 错误的做法:使用 index 作为 key const TodoList = () => {const [todos, setTodos] = useState(['Learn React', 'Build a project']);const addTodo = () => {const newTodo = prompt('New todo:');// 在列表开头添加新项setTodos([newTodo, ...todos]);};return (<div><button onClick={addTodo}>Add Todo</button><ul>{todos.map((todo, index) => (// 🚨 危险!使用 index 作为 key<li key={index}>{todo}<input type="text" placeholder="Edit..." /></li>))}</ul></div>); };
会发生什么?
初始有两个待办项:Learn React 和 Build a project。它们的 key 分别是 0 和 1。
你在 第二项 (key=1) 的输入框里输入了 “test”。
你点击 “Add Todo”,在开头添加了一个新项 “New Item”。
现在列表变成了:[‘New Item’, ‘Learn React’, ‘Build a project’]。
React 开始 Diffing:
旧 key=0 (‘Learn React’) vs 新 key=0 (‘New Item’) -> 内容变了,更新文本,但输入框实例被复用。
旧 key=1 (‘Build a project’) vs 新 key=1 (‘Learn React’) -> 内容变了,更新文本,输入框实例被复用(现在它显示之前输入的 “test”!)。
创建一个新的 key=2 的项 (‘Build a project’)。
结果:你原本在第二项输入的 “test”,神奇地“跳”到了现在第二项(‘Learn React’)的输入框里!这显然不是用户想要的行为。
如何修复?
// 正确的做法:使用唯一的 id 作为 key {todos.map((todo) => (<li key={todo.id}> {/* 假设每个 todo 对象都有唯一的 id */}{todo.text}<input type="text" placeholder="Edit..." /></li> ))}