淺談React中key的作用
key 概念
在 React 中,key 用于識別哪些元素是變化、添加或刪除的。
在列表渲染中,key 尤其重要,因?yàn)樗芴岣咪秩拘阅芎痛_保組件狀態(tài)的一致性。
key 的作用
1)唯一性標(biāo)識:
React 通過 key 唯一標(biāo)識列表中的每個元素。當(dāng)列表發(fā)生變化(增刪改排序)時,React 會通過 key 快速判斷:
- 哪些元素是新增的(需要創(chuàng)建新 DOM 節(jié)點(diǎn))
- 哪些元素是移除的(需要銷毀舊 DOM 節(jié)點(diǎn))
- 哪些元素是移動的(直接復(fù)用現(xiàn)有 DOM 節(jié)點(diǎn),僅調(diào)整順序)
如果沒有 key,React 會默認(rèn)使用數(shù)組索引(index)作為標(biāo)識,這在動態(tài)列表中會導(dǎo)致 性能下降 或 狀態(tài)錯誤。
2)保持組件狀態(tài):
使用 key 能確保組件在更新過程中狀態(tài)的一致性。不同的 key 會使 React 認(rèn)為它們是不同的組件實(shí)例,因而會創(chuàng)建新的組件實(shí)例,而不是重用現(xiàn)有實(shí)例。這對于有狀態(tài)的組件尤為重要。
// 如果初始列表是 [A, B],用索引 index 作為 key:
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
// 在頭部插入新元素變?yōu)?[C, A, B] 時:
// React 會認(rèn)為 key=0 → C(重新創(chuàng)建)
// key=1 → A(復(fù)用原 key=0 的 DOM,但狀態(tài)可能殘留)
// 此時,原本屬于 A 的輸入框狀態(tài)可能會錯誤地出現(xiàn)在 C 中。
3)高效的 Diff 算法:
在列表中使用 key 屬性,React 可以通過 Diff 算法快速比較新舊元素,確定哪些元素需要重新渲染,哪些元素可以復(fù)用。這減少了不必要的 DOM 操作,從而提高渲染性能。
源碼解析
以下是 React 源碼中與 key 相關(guān)的關(guān)鍵部分:
1)生成 Fiber樹
在生成 Fiber 樹時,React 使用 key 來匹配新舊節(jié)點(diǎn)。
src/react/packages/react-reconciler/src/ReactChildFiber.js
// * 協(xié)調(diào)子節(jié)點(diǎn),構(gòu)建新的子fiber結(jié)構(gòu),并且返回新的子fiber
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null, // 老fiber的第一個子節(jié)點(diǎn)
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. 從左邊往右遍歷,比較新老節(jié)點(diǎn),如果節(jié)點(diǎn)可以復(fù)用,繼續(xù)往右,否則就停止
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 新節(jié)點(diǎn)沒了,(老節(jié)點(diǎn)還有)。則刪除剩余的老節(jié)點(diǎn)即可
// 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 (新節(jié)點(diǎn)還有),老節(jié)點(diǎn)沒了
// 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 新老節(jié)點(diǎn)都還有節(jié)點(diǎn),但是因?yàn)槔蟜iber是鏈表,不方便快速get與delete,
// ! 因此把老fiber鏈表中的節(jié)點(diǎn)放入Map中,后續(xù)操作這個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. 如果是組件更新階段,此時新節(jié)點(diǎn)已經(jīng)遍歷完了,能復(fù)用的老節(jié)點(diǎn)都用完了,
// ! 則最后查找Map里是否還有元素,如果有,則證明是新節(jié)點(diǎn)里不能復(fù)用的,也就是要被刪除的元素,此時刪除這些元素就可以了
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 中的關(guān)鍵使用:
頂層“單個元素”分支(如 reconcileSingleElement):先在兄弟鏈表里按 key 查找可復(fù)用的老 Fiber;若 key 相同再比類型,復(fù)用成功則刪除其他老兄弟,否則刪到尾并新建。
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 檢查老的fiber單鏈表中是否有可以復(fù)用的節(jié)點(diǎn)
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 取出來按數(shù)組/迭代器邏輯繼續(xù)走。
2)比較新舊節(jié)點(diǎn)
在比較新舊節(jié)點(diǎn)時,React 通過 key 來確定節(jié)點(diǎn)是否相同:
src/react/packages/react-reconciler/src/ReactChildFiber.js
function 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;
}
實(shí)際案例
1)簡單列表
假設(shè)我們有一個簡單的列表:
const items = this.state.items.map(item =>
<li key={item.id}>{ item.text }</li>
)
在上述代碼中,每個
元素都有一個唯一的 key。
如果 items 數(shù)組發(fā)生變化(如添加或刪除元素),React將根據(jù) key 來高效地更新DOM:
2)錯誤案例演示

import React, { useState } from 'react'
// 錯誤案例:使用數(shù)組索引作為 key,導(dǎo)致組件在插入/重排時狀態(tài)錯亂
// 復(fù)現(xiàn)實(shí)驗(yàn):
// 1) 在下方兩個輸入框分別輸入不同文本(對應(yīng) A、B)
// 2) 點(diǎn)擊“在頭部插入 C” → 列表從 [A, B] 變?yōu)?[C, A, B]
// 3) 使用 index 作為 key 時:
// key=0 → C(重新創(chuàng)建)
// key=1 → A(復(fù)用原 key=0 的 DOM,狀態(tài)可能殘留)
// 因此原本屬于 A 的輸入框狀態(tài)可能會錯誤地出現(xiàn)在 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="在此輸入以觀察狀態(tài)"
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(頭部插入觸發(fā)狀態(tài)錯亂)</h3>
<button onClick={prependC} style={{ marginBottom: 12 }}>
在頭部插入 C
</button>
{labels.map((label, index) => (
// 錯誤:使用 index 作為 key,頭部插入 C 后會發(fā)生狀態(tài)錯位
<InputItem key={index} label={label} />
))}
</div>
)
}
- 當(dāng)一個元素被刪除時,React僅刪除對應(yīng) key 的DOM節(jié)點(diǎn)。
- 當(dāng)一個元素被添加時,React 僅在相應(yīng)的位置插入新的DOM節(jié)點(diǎn)。
- 當(dāng)一個元素被移動時,React 會識別到位置變化并重新排列 DOM 節(jié)點(diǎn)。
到此這篇關(guān)于淺談React中key的作用的文章就介紹到這了,更多相關(guān)React key內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
useEffect如何通過form.getFieldValue(‘xxx‘)監(jiān)聽Form表單變化
這篇文章主要介紹了useEffect如何通過form.getFieldValue(‘xxx‘)監(jiān)聽Form表單變化問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
React如何將組件渲染到指定DOM節(jié)點(diǎn)詳解
這篇文章主要給大家介紹了關(guān)于React如何將組件渲染到指定DOM節(jié)點(diǎn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)下吧。2017-09-09
react反向代理使用http-proxy-middleware問題
這篇文章主要介紹了react反向代理使用http-proxy-middleware問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07
React實(shí)現(xiàn)復(fù)雜搜索表單的展開收起功能
本節(jié)對于需要展開收起效果的查詢表單進(jìn)行概述,主要涉及前端樣式知識。對React實(shí)現(xiàn)復(fù)雜搜索表單的展開-收起功能感興趣的朋友一起看看吧2021-09-09
在React中寫一個Animation組件為組件進(jìn)入和離開加上動畫/過度效果
這篇文章主要介紹了在React中寫一個Animation組件為組件進(jìn)入和離開加上動畫/過度效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-06-06
使用webpack5從0到1搭建一個react項(xiàng)目的實(shí)現(xiàn)步驟
這篇文章主要介紹了使用webpack5從0到1搭建一個react項(xiàng)目的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
使用useMutation和React Query發(fā)布數(shù)據(jù)demo
這篇文章主要為大家介紹了使用useMutation和React Query發(fā)布數(shù)據(jù)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

