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