React 中 key 的作用是什么?
Date: August 31, 2025
Area: 原理
key 概念
在 React 中,key 用于识别哪些元素是变化、添加或删除的。
在列表渲染中,key 尤其重要,因为它能提高渲染性能和确保组件状态的一致性。
key 的作用
1)唯一性标识:
React 通过 key
唯一标识列表中的每个元素。当列表发生变化(增删改排序)时,React 会通过 key
快速判断:
- 哪些元素是新增的(需要创建新 DOM 节点)
- 哪些元素是移除的(需要销毁旧 DOM 节点)
- 哪些元素是移动的(直接复用现有 DOM 节点,仅调整顺序)
如果没有 key
,React 会默认使用数组索引(index
)作为标识,这在动态列表中会导致 性能下降 或 状态错误。
2)保持组件状态:
使用 key 能确保组件在更新过程中状态的一致性。不同的 key 会使 React 认为它们是不同的组件实例,因而会创建新的组件实例,而不是重用现有实例。这对于有状态的组件尤为重要。
jsx
// 如果初始列表是 [A, B],用索引 index 作为 key:
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
// 在头部插入新元素变为 [C, A, B] 时:
// React 会认为 key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,但状态可能残留)
// 此时,原本属于 A 的输入框状态可能会错误地出现在 C 中。
3)高效的 Diff 算法:
在列表中使用 key 属性,React 可以通过 Diff 算法快速比较新旧元素,确定哪些元素需要重新渲染,哪些元素可以复用。这减少了不必要的 DOM 操作,从而提高渲染性能。
源码解析
以下是 React 源码中与 key 相关的关键部分:
1)生成 Fiber树
在生成 Fiber 树时,React 使用 key 来匹配新旧节点。
src/react/packages/react-reconciler/src/ReactChildFiber.js
-
Code:
jsx// * 协调子节点,构建新的子fiber结构,并且返回新的子fiber function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, // 老fiber的第一个子节点 newChild: any, lanes: Lanes, ): Fiber | null { // This indirection only exists so we can reset `thenableState` at the end. // It should get inlined by Closure. thenableIndexCounter = 0; const firstChildFiber = reconcileChildFibersImpl( returnFiber, currentFirstChild, newChild, lanes, null, // debugInfo ); thenableState = null; // Don't bother to reset `thenableIndexCounter` to 0 because it always gets // set at the beginning. return firstChildFiber; } function reconcileChildrenArray( returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<any>, lanes: Lanes, debugInfo: ReactDebugInfo | null, ): Fiber | null { let resultingFirstChild: Fiber | null = null; // 存储新生成的child let previousNewFiber: Fiber | null = null; let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; // ! 1. 从左边往右遍历,比较新老节点,如果节点可以复用,继续往右,否则就停止 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } const newFiber = updateSlot( returnFiber, oldFiber, newChildren[newIdx], lanes, debugInfo, ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need // a better way to communicate whether this was a miss or null, // boolean, undefined, etc. if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. deleteChild(returnFiber, oldFiber); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { // TODO: Defer siblings if we're not at the right index for this slot. // I.e. if we had null values before, then we want to defer this // for each null value. However, we also don't want to call updateSlot // with the previous one. previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; } // !2.1 新节点没了,(老节点还有)。则删除剩余的老节点即可 // 0 1 2 3 4 // 0 1 2 3 if (newIdx === newChildren.length) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); if (getIsHydrating()) { const numberOfForks = newIdx; pushTreeFork(returnFiber, numberOfForks); } return resultingFirstChild; } // ! 2.2 (新节点还有),老节点没了 // 0 1 2 3 4 // 0 1 2 3 4 5 if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild( returnFiber, newChildren[newIdx], lanes, debugInfo, ); if (newFiber === null) { continue; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } if (getIsHydrating()) { const numberOfForks = newIdx; pushTreeFork(returnFiber, numberOfForks); } return resultingFirstChild; } // !2.3 新老节点都还有节点,但是因为老fiber是链表,不方便快速get与delete, // ! 因此把老fiber链表中的节点放入Map中,后续操作这个Map的get与delete // 0 1| 4 5 // 0 1| 7 8 2 3 // Add all children to a key map for quick lookups. const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, debugInfo, ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // The new fiber is a work in progress, but if there exists a // current, that means that we reused the fiber. We need to delete // it from the child list so that we don't add it to the deletion // list. existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key, ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } // !3. 如果是组件更新阶段,此时新节点已经遍历完了,能复用的老节点都用完了, // ! 则最后查找Map里是否还有元素,如果有,则证明是新节点里不能复用的,也就是要被删除的元素,此时删除这些元素就可以了 if (shouldTrackSideEffects) { // Any existing children that weren't consumed above were deleted. We need // to add them to the deletion list. existingChildren.forEach(child => deleteChild(returnFiber, child)); } if (getIsHydrating()) { const numberOfForks = newIdx; pushTreeFork(returnFiber, numberOfForks); } return resultingFirstChild; }
在 reconcileChildFibers 中的关键使用:
顶层"单个元素"分支(如 reconcileSingleElement):先在兄弟链表里按 key 查找可复用的老 Fiber;若 key 相同再比类型,复用成功则删除其他老兄弟,否则删到尾并新建。
jsx
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 检查老的fiber单链表中是否有可以复用的节点
while (child !== null) {
if (child.key === key) {
...
if (child.elementType === elementType || ... ) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
...
return existing;
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
}
...
}
- 顶层对 Fragment(无 key)特殊处理:若是未带 key 的顶层 Fragment,会直接把 children 取出来按数组/迭代器逻辑继续走。
2)比较新旧节点
在比较新旧节点时,React 通过 key 来确定节点是否相同:
src/react/packages/react-reconciler/src/ReactChildFiber.js
-
Code:
jsxfunction updateSlot( returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, lanes: Lanes, debugInfo: null | ReactDebugInfo, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. const key = oldFiber !== null ? oldFiber.key : null; if ( (typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number' ) { // Text nodes don't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a text // node. if (key !== null) { return null; } return updateTextNode( returnFiber, oldFiber, '' + newChild, lanes, debugInfo, ); } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { return updateElement( returnFiber, oldFiber, newChild, lanes, mergeDebugInfo(debugInfo, newChild._debugInfo), ); } else { return null; } } case REACT_PORTAL_TYPE: { if (newChild.key === key) { return updatePortal( returnFiber, oldFiber, newChild, lanes, debugInfo, ); } else { return null; } } case REACT_LAZY_TYPE: { const payload = newChild._payload; const init = newChild._init; return updateSlot( returnFiber, oldFiber, init(payload), lanes, mergeDebugInfo(debugInfo, newChild._debugInfo), ); } } if (isArray(newChild) || getIteratorFn(newChild)) { if (key !== null) { return null; } return updateFragment( returnFiber, oldFiber, newChild, lanes, null, mergeDebugInfo(debugInfo, newChild._debugInfo), ); } // Usable node types // // Unwrap the inner value and recursively call this function again. if (typeof newChild.then === 'function') { const thenable: Thenable<any> = (newChild: any); return updateSlot( returnFiber, oldFiber, unwrapThenable(thenable), lanes, debugInfo, ); } if (newChild.$$typeof === REACT_CONTEXT_TYPE) { const context: ReactContext<mixed> = (newChild: any); return updateSlot( returnFiber, oldFiber, readContextDuringReconcilation(returnFiber, context, lanes), lanes, debugInfo, ); } throwOnInvalidObjectType(returnFiber, newChild); } if (__DEV__) { if (typeof newChild === 'function') { warnOnFunctionType(returnFiber, newChild); } if (typeof newChild === 'symbol') { warnOnSymbolType(returnFiber, newChild); } } return null; }
实际案例
1)简单列表
假设我们有一个简单的列表:
jsx
const items = this.state.items.map(item =>
<li key={item.id}>{ item.text }</li>
)
在上述代码中,每个
- 元素都有一个唯一的 key。
如果 items 数组发生变化(如添加或删除元素),React将根据 key 来高效地更新DOM:
- 当一个元素被删除时,React仅删除对应 key 的DOM节点。
- 当一个元素被添加时,React 仅在相应的位置插入新的DOM节点。
- 当一个元素被移动时,React 会识别到位置变化并重新排列 DOM 节点。
2)错误案例演示

tsx
import React, { useState } from 'react'
// 错误案例:使用数组索引作为 key,导致组件在插入/重排时状态错乱
// 复现实验:
// 1) 在下方两个输入框分别输入不同文本(对应 A、B)
// 2) 点击"在头部插入 C" → 列表从 [A, B] 变为 [C, A, B]
// 3) 使用 index 作为 key 时:
// key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,状态可能残留)
// 因此原本属于 A 的输入框状态可能会错误地出现在 C 中
function InputItem({ label }: { label: string }) {
const [text, setText] = useState<string>('')
return (
<div
style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}
>
<span style={{ width: 80 }}>{label}</span>
<input
placeholder="在此输入以观察状态"
value={text}
onChange={e => setText(e.target.value)}
/>
</div>
)
}
export default function TestDemo() {
const [labels, setLabels] = useState<string[]>(['A', 'B'])
const prependC = () => {
setLabels(prev => ['C', ...prev])
}
return (
<div style={{ padding: 16 }}>
<h3>错误示例:使用 index 作为 key(头部插入触发状态错乱)</h3>
<button onClick={prependC} style={{ marginBottom: 12 }}>
在头部插入 C
</button>
{labels.map((label, index) => (
// 错误:使用 index 作为 key,头部插入 C 后会发生状态错位
<InputItem key={index} label={label} />
))}
</div>
)
}