React 中 key 的作用

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 認為它們是不同的組件實例,因而會創建新的組件實例,而不是重用現有實例。這對于有狀態的組件尤為重要。

// 如果初始列表是 [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:

        // * 協調子節點,構建新的子fiber結構,并且返回新的子fiberfunction 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; // 存儲新生成的childlet 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 3if (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 5if (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 相同再比類型,復用成功則刪除其他老兄弟,否則刪到尾并新建。

  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:

      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;}
    

實際案例

1)簡單列表

假設我們有一個簡單的列表:

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)錯誤案例演示

    在這里插入圖片描述

    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 (<divstyle={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}><span style={{ width: 80 }}>{label}</span><inputplaceholder="在此輸入以觀察狀態"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>)
    }

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/97733.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/97733.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/97733.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

wpf之附加屬性

前言 附加屬性是 WPF 中一個非常強大和獨特的概念。簡單來說&#xff0c;它允許一個對象為另一個在其本身類定義中未定義的屬性賦值。 1、定義附加屬性 定義一個Watermark的附加屬性&#xff0c;該屬性的作用是將TextBox的附加屬性改變時&#xff0c;TextBox的字體顏色改成灰…

深入淺出 RabbitMQ-消息可靠性投遞

大家好&#xff0c;我是工藤學編程 &#x1f989;一個正在努力學習的小博主&#xff0c;期待你的關注實戰代碼系列最新文章&#x1f609;C實現圖書管理系統&#xff08;Qt C GUI界面版&#xff09;SpringBoot實戰系列&#x1f437;【SpringBoot實戰系列】SpringBoot3.X 整合 Mi…

數字化時代,中小企業如何落地數字化轉型

大數據時代&#xff0c;各行各業的行業龍頭和大型集團都已經開始了數據管理&#xff0c;讓數據成為數據資產。但是在我國&#xff0c;中小企業的數量巨大&#xff0c;很多管理者忽視了這一點&#xff0c;今天我們就來聊一聊中小企業的數字化轉型。中小企業需要數字化轉型首先要…

Unity筆記(九)——畫線功能Linerenderer、范圍檢測、射線檢測

寫在前面&#xff1a;寫本系列(自用)的目的是回顧已經學過的知識、記錄新學習的知識或是記錄心得理解&#xff0c;方便自己以后快速復習&#xff0c;減少遺忘。這里只記錄代碼知識。十一、畫線功能Linerenderer畫線功能Linerenderer是Unity提供的畫線腳本&#xff0c;創建一個空…

刷題記錄(8)string類操作使用

一、僅反轉字母 917. 僅僅反轉字母 - 力扣&#xff08;LeetCode&#xff09; 簡單來說輸入字符串&#xff0c;要求你返回所有僅字母位置反轉后的字符串。 簡單看一個樣例加深理解&#xff1a; 前后互換&#xff0c;我想思路基本很明顯了&#xff0c;雙指針&#xff0c;或者說…

用好AI,從提示詞工程到上下文工程

前言 隨著 AI 大模型的爆發,提示詞工程(prompt engineering ) 一度是用戶應用 AI ,發揮 AI 能力最重要、也最應該掌握的技術。 但現在,在 “提示詞工程”的基礎上,一個更寬泛也更強力的演化概念被提出,也就是本文我們要介紹的 “上下文工程(Context Engineering)” …

計算機Python畢業設計推薦:基于Django+Vue用戶評論挖掘旅游系統

精彩專欄推薦訂閱&#xff1a;在下方主頁&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f496;&#x1f525;作者主頁&#xff1a;計算機畢設木哥&#x1f525; &#x1f496; 文章目錄 一、項目介紹二、…

? 肆 ? ? 默認安全:安全建設方案 ? a.信息安全基線

&#x1f44d;點「贊」&#x1f4cc;收「藏」&#x1f440;關「注」&#x1f4ac;評「論」 在金融科技深度融合的背景下&#xff0c;信息安全已從單純的技術攻防擴展至架構、合規、流程與創新的系統工程。作為一名從業十多年的老兵&#xff0c;將系統闡述數字銀行安全體系的建設…

如何用AI視頻增強清晰度軟件解決畫質模糊問題

在視頻制作和分享過程中&#xff0c;畫質模糊、細節丟失等問題常常影響觀看體驗。無論是老舊視頻的修復還是低分辨率素材的優化&#xff0c;清晰度提升都成為用戶關注的重點。借助專業的AI技術&#xff0c;這些問題可以得到有效解決。目前市面上存在多種解決方案&#xff0c;能…

Linux92 shell:倒計時,用戶分類

問題 while IFS read -r line;doootweb kk]# tail -6 /etc/passwd user1r4:x:1040:1040::/home/user1r4:/bin/bash useros20:x:1041:1041::/home/useros20:/bin/bash useros21:x:1042:1042::/home/useros21:/bin/bash useros22:x:1043:1043::/home/useros22:/bin/bash useros23…

LinkedList源碼解析

1. 數據結構設計 (1) 節點結構 LinkedList 的核心是雙向鏈表節點 Node&#xff1a; private static class Node<E> {E item; // 存儲的元素Node<E> next; // 后繼節點Node<E> prev; // 前驅節點Node(Node<E> prev, E element, Node<E&g…

語雀批量導出知識庫

使用工具&#xff1a;yuque-dl 參考文檔&#xff1a; GitHub - gxr404/yuque-dl: yuque 語雀知識庫下載 Yuque-DL&#xff1a;一款強大的語雀資源下載工具_語雀文檔怎么下載-CSDN博客

電子電氣架構 --- 當前企業EEA現狀(下)

我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 做到欲望極簡,了解自己的真實欲望,不受外在潮流的影響,不盲從,不跟風。把自己的精力全部用在自己。一是去掉多余,凡事找規律,基礎是誠信;二是…

flink中的窗口的介紹

本文重點 無界流會源源不斷的產生數據,有的時候我們需要把無界流進行切分成一段一段的有界數據,把一段內的所有數據看成一個整體進行聚合計算,這是實現無界流轉成有界流的方式之一。 為什么需要窗口 數據是源源不斷產生的,我們可能只關心某個周期內的統計結果。比如電費…

自建es 通過Flink同步mysql數據 Docker Compose

資源es:7.18 kibana:7.18 flink:1.17.2目錄mkdir -p /usr/project/flink/{conf,job,logs} chmod -R 777 /usr/project/flink #資源情況 mysql8.0 Elasticsearch7.18 自建# 目錄結構 /usr/project/flink/ /usr/project/flink/ ├── conf/ │ ├── flink-conf.yaml │ └…

AI瀏覽器和釘釘ONE是不是偽需求?

最近兩則新聞格外引起了我的注意&#xff1a;一是Claude推出了官方瀏覽器插件&#xff0c;二是釘釘發布了釘釘ONE。前者說明AI瀏覽器未必有必要&#xff0c;后者則描繪了一幅“刷刷手機就能完成工作”的未來辦公圖景。這幾天我經常在思考&#xff0c;AI瀏覽器是不是沒有必要&am…

從結構化到多模態:RAG文檔解析工具選型全指南

在RAG系統建設中&#xff0c;文檔解析質量直接決定最終效果上限&#xff0c;選擇合適的解析工具已成為避免"垃圾進&#xff0c;垃圾出"&#xff08;GIGO&#xff09;困境的關鍵決策。一、文檔解析&#xff1a;RAG系統的基石與瓶頸 當前企業知識庫中超過80%的信息存儲…

設計模式:享元模式(Flyweight Pattern)

文章目錄一、享元模式的介紹二、實例分析三、示例代碼一、享元模式的介紹 享元模式&#xff08;Flyweight Pattern&#xff09; 是一種結構型設計模式。通過共享相同對象&#xff0c;減少內存消耗&#xff0c;提高性能。 它摒棄了在每個對象中保存所有數據的方式&#xff0c; 通…

【Go語言入門教程】 Go語言的起源與技術特點:從誕生到現代編程利器(一)

文章目錄前言1. Go語言的起源與發展2. Go語言的核心設計團隊2.1 Ken Thompson&#xff08;肯湯普森&#xff09;2.2 Rob Pike&#xff08;羅布派克&#xff09;2.3 Robert Griesemer&#xff08;羅伯特格瑞澤默&#xff09;設計動機&#xff1a;解決C的痛點3. Go語言的核心特性…

rocketmq啟動與測試

1.更改runserver.sh的內存大小 vi runserver.sh 2.更改 runbroker.sh內存大小 vi runbroker.sh3.設置環境變量 vi ~/.bash_profile 新增 export NAMESRV_ADDRlocalhost:98764.啟動 --在bin的上一級目錄啟動 nohup bin/mqnamesrv & nohup bin/mqbroker &5.查看日志 le…