淺談React中key的作用
key 概念
在 React 中,key 用于識別哪些元素是變化、添加或刪除的。
在列表渲染中,key 尤其重要,因?yàn)樗芴岣咪秩拘阅芎痛_保組件狀態(tài)的一致性。
key 的作用
1)唯一性標(biāo)識:
React 通過 key
唯一標(biāo)識列表中的每個(gè)元素。當(dāng)列表發(fā)生變化(增刪改排序)時(shí),React 會(huì)通過 key
快速判斷:
- 哪些元素是新增的(需要?jiǎng)?chuàng)建新 DOM 節(jié)點(diǎn))
- 哪些元素是移除的(需要銷毀舊 DOM 節(jié)點(diǎn))
- 哪些元素是移動(dòng)的(直接復(fù)用現(xiàn)有 DOM 節(jié)點(diǎn),僅調(diào)整順序)
如果沒有 key
,React 會(huì)默認(rèn)使用數(shù)組索引(index
)作為標(biāo)識,這在動(dòng)態(tài)列表中會(huì)導(dǎo)致 性能下降 或 狀態(tài)錯(cuò)誤。
2)保持組件狀態(tài):
使用 key 能確保組件在更新過程中狀態(tài)的一致性。不同的 key 會(huì)使 React 認(rèn)為它們是不同的組件實(shí)例,因而會(huì)創(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] 時(shí): // React 會(huì)認(rèn)為 key=0 → C(重新創(chuàng)建) // key=1 → A(復(fù)用原 key=0 的 DOM,但狀態(tài)可能殘留) // 此時(shí),原本屬于 A 的輸入框狀態(tài)可能會(huì)錯(cuò)誤地出現(xiàn)在 C 中。
3)高效的 Diff 算法:
在列表中使用 key 屬性,React 可以通過 Diff 算法快速比較新舊元素,確定哪些元素需要重新渲染,哪些元素可以復(fù)用。這減少了不必要的 DOM 操作,從而提高渲染性能。
源碼解析
以下是 React 源碼中與 key 相關(guān)的關(guān)鍵部分:
1)生成 Fiber樹
在生成 Fiber 樹時(shí),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的第一個(gè)子節(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; // 存儲(chǔ)新生成的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ù)操作這個(gè)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. 如果是組件更新階段,此時(shí)新節(jié)點(diǎn)已經(jīng)遍歷完了,能復(fù)用的老節(jié)點(diǎn)都用完了, // ! 則最后查找Map里是否還有元素,如果有,則證明是新節(jié)點(diǎn)里不能復(fù)用的,也就是要被刪除的元素,此時(shí)刪除這些元素就可以了 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)鍵使用:
頂層“單個(gè)元素”分支(如 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,會(huì)直接把 children 取出來按數(shù)組/迭代器邏輯繼續(xù)走。
2)比較新舊節(jié)點(diǎn)
在比較新舊節(jié)點(diǎn)時(shí),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è)我們有一個(gè)簡單的列表:
const items = this.state.items.map(item => <li key={item.id}>{ item.text }</li> )
在上述代碼中,每個(gè)
元素都有一個(gè)唯一的 key。
如果 items 數(shù)組發(fā)生變化(如添加或刪除元素),React將根據(jù) key 來高效地更新DOM:
2)錯(cuò)誤案例演示
import React, { useState } from 'react' // 錯(cuò)誤案例:使用數(shù)組索引作為 key,導(dǎo)致組件在插入/重排時(shí)狀態(tài)錯(cuò)亂 // 復(fù)現(xiàn)實(shí)驗(yàn): // 1) 在下方兩個(gè)輸入框分別輸入不同文本(對應(yīng) A、B) // 2) 點(diǎn)擊“在頭部插入 C” → 列表從 [A, B] 變?yōu)?[C, A, B] // 3) 使用 index 作為 key 時(shí): // key=0 → C(重新創(chuàng)建) // key=1 → A(復(fù)用原 key=0 的 DOM,狀態(tài)可能殘留) // 因此原本屬于 A 的輸入框狀態(tài)可能會(huì)錯(cuò)誤地出現(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>錯(cuò)誤示例:使用 index 作為 key(頭部插入觸發(fā)狀態(tài)錯(cuò)亂)</h3> <button onClick={prependC} style={{ marginBottom: 12 }}> 在頭部插入 C </button> {labels.map((label, index) => ( // 錯(cuò)誤:使用 index 作為 key,頭部插入 C 后會(huì)發(fā)生狀態(tài)錯(cuò)位 <InputItem key={index} label={label} /> ))} </div> ) }
- 當(dāng)一個(gè)元素被刪除時(shí),React僅刪除對應(yīng) key 的DOM節(jié)點(diǎn)。
- 當(dāng)一個(gè)元素被添加時(shí),React 僅在相應(yīng)的位置插入新的DOM節(jié)點(diǎn)。
- 當(dāng)一個(gè)元素被移動(dòng)時(shí),React 會(huì)識別到位置變化并重新排列 DOM 節(jié)點(diǎn)。
到此這篇關(guān)于淺談React中key的作用的文章就介紹到這了,更多相關(guān)React key內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React報(bào)錯(cuò)之組件不能作為JSX組件使用的解決方法
本文主要介紹了React報(bào)錯(cuò)之組件不能作為JSX組件使用的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07React模仿網(wǎng)易云音樂實(shí)現(xiàn)一個(gè)音樂項(xiàng)目詳解流程
這篇文章主要介紹了React模仿網(wǎng)易云音樂實(shí)現(xiàn)一個(gè)音樂項(xiàng)目的詳細(xì)流程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08JavaScript React如何修改默認(rèn)端口號方法詳解
這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號方法詳解,文中通過步驟圖片解析介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07react中如何使用定義數(shù)據(jù)并監(jiān)聽其值
這篇文章主要介紹了react中如何使用定義數(shù)據(jù)并監(jiān)聽其值問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01react?app?rewrited替代品craco使用示例
這篇文章主要為大家介紹了react?app?rewrited替代品craco使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11