04. 列表渲染优化 - key 与虚拟化一、5W1H 概述维度内容What使用稳定的 key 标识列表项使用虚拟列表处理大数据Why避免不必要的 DOM 操作处理大量数据时的性能问题When动态列表、大数据量渲染1000 条Where列表渲染组件中Who处理列表数据的开发者How使用唯一 ID 作为 key使用 react-window 实现虚拟列表二、What - 什么是 key 和虚拟化2.1 key 的作用key 是 React 用来识别列表项的唯一标识符帮助 React 确定哪些元素被添加、更改或删除。2.2 虚拟列表虚拟列表只渲染可视区域内的元素滚动时动态替换从而处理大量数据。三、Why - 为什么需要这些优化3.1 key 的重要性没有 key 时 [1,2,3] → [1,2,3,4] React 会重新渲染整个列表 有 key 时 [1,2,3] → [1,2,3,4] React 只添加新的第 4 项3.2 虚拟列表的必要性渲染 10000 条数据时直接渲染所有 DOM 节点会导致性能问题。// ❌ 渲染 10000 条数据性能差 function BadList({ items }) { return ( ul {items.map(item ( li key{item.id}{item.text}/li ))} /ul ); } // ✅ 使用虚拟列表 import { FixedSizeList as List } from react-window; function GoodList({ items }) { const Row ({ index, style }) ( div style{style}{items[index].text}/div ); return ( List height{400} itemCount{items.length} itemSize{35} width100% {Row} /List ); }四、When - 何时使用场景优化方式动态列表稳定 key列表顺序可能变化稳定 key不要用索引大数据量1000虚拟列表无限滚动虚拟列表 懒加载表格数据虚拟表格五、Where - 在哪里使用列表渲染组件中使用 key 属性使用虚拟列表库六、Who - 谁需要使用所有处理列表数据的 React 开发者。七、How - 如何使用7.1 选择合适的 key// ✅ 好的 Key使用数据中的唯一 ID li key{todo.id}{todo.text}/li // ✅ 可以使用索引仅在列表静态且不会重新排序时 li key{index}{todo.text}/li // ❌ 坏的 Key使用随机数 li key{Math.random()}{todo.text}/li7.2 key 的最佳实践场景推荐 Key说明数据库数据数据库 ID最稳定本地生成数据uuid / nanoid保证唯一性静态列表索引谨慎列表不变时可接受动态列表唯一标识符必须稳定且唯一7.3 使用索引作为 key 的问题// ❌ 当列表顺序变化时会有问题 {todos.map((todo, index) ( TodoItem key{index} todo{todo} / ))} // 问题 // 1. 删除第一项时所有后续项的 key 都改变了 // 2. React 会重新渲染所有项而不是只删除一项 // 3. 可能导致组件状态错乱7.4 组合 key// 当没有唯一 ID 时可以组合多个字段 {items.map(item ( div key{${item.type}-${item.id}} {item.value} /div ))}7.5 react-window 固定高度列表npminstallreact-windowimport { FixedSizeList as List } from react-window; const Row ({ index, style, data }) ( div style{style} {data[index]} - 第 {index 1} 项 /div ); function VirtualList({ items }) { return ( List height{400} itemCount{items.length} itemSize{35} width100% itemData{items} {Row} /List ); }7.6 react-window 可变高度列表import { VariableSizeList as List } from react-window; const rowHeights new Array(1000).fill(50); const Row ({ index, style, data }) ( div style{style} 第 {index} 项 - 高度可变 /div ); function VariableList({ items }) { const getItemSize (index) rowHeights[index]; return ( List height{400} itemCount{items.length} itemSize{getItemSize} width100% {Row} /List ); }7.7 react-window 网格布局import { FixedSizeGrid as Grid } from react-window; const Cell ({ columnIndex, rowIndex, style, data }) ( div style{style} 行 {rowIndex}, 列 {columnIndex} /div ); function VirtualGrid() { return ( Grid columnCount{3} columnWidth{200} height{400} rowCount{100} rowHeight{50} width{600} {Cell} /Grid ); }7.8 无限滚动 虚拟列表import { FixedSizeList as List } from react-window; import InfiniteLoader from react-window-infinite-loader; function InfiniteList() { const [items, setItems] useState([]); const [hasNextPage, setHasNextPage] useState(true); const loadMoreItems async (startIndex, stopIndex) { const newItems await fetchItems(startIndex, stopIndex); setItems(prev [...prev, ...newItems]); setHasNextPage(newItems.length 0); }; const isItemLoaded (index) !hasNextPage || index items.length; const Row ({ index, style }) ( div style{style} {isItemLoaded(index) ? items[index] : 加载中...} /div ); return ( InfiniteLoader isItemLoaded{isItemLoaded} itemCount{items.length 100} loadMoreItems{loadMoreItems} {({ onItemsRendered, ref }) ( List height{400} itemCount{items.length} itemSize{35} onItemsRendered{onItemsRendered} ref{ref} width100% {Row} /List )} /InfiniteLoader ); }7.9 虚拟表格import { FixedSizeList as List } from react-window; function VirtualTable({ data }) { const Row ({ index, style }) { const row data[index]; return ( div style{style} classNametable-row div classNamecell{row.id}/div div classNamecell{row.name}/div div classNamecell{row.email}/div /div ); }; return ( div div classNametable-header div classNamecellID/div div classNamecell姓名/div div classNamecell邮箱/div /div List height{400} itemCount{data.length} itemSize{35} width100% {Row} /List /div ); }7.10 性能优化技巧// 1. 使用 React.memo 优化列表项 const Row React.memo(({ index, style, data }) { return div style{style}{data[index]}/div; }); // 2. 避免内联函数 // ❌ 每次渲染创建新函数 List rowRenderer{({ index, style }) ( div style{style}{items[index]}/div )} / // ✅ 提取组件 const Row ({ index, style, data }) ( div style{style}{data[index]}/div ); List rowRenderer{Row} itemData{items} / // 3. 设置 overscanCount List overscanCount{5} // 预渲染上下各5行 // ... /八、常见陷阱8.1 Key 不唯一// ❌ Key 必须唯一 {items.map(item ( div key{item.type} {/* 如果有相同 type 会报错 */} {item.value} /div ))} // ✅ 使用组合 key {items.map(item ( div key{${item.type}-${item.id}} {item.value} /div ))}8.2 Key 不会传递给组件// ❌ 不能通过 props 获取 key function MyComponent(props) { console.log(props.key); // undefined return div{props.value}/div; } // ✅ 显式传递 {items.map(item ( MyComponent key{item.id} id{item.id} value{item.value} / ))}8.3 虚拟列表容器高度// ❌ 容器没有固定高度 div List heightauto ... / {/* 不会工作 */} /div // ✅ 容器必须有固定高度 div style{{ height: 400px }} List height{400} ... / /div九、练习题基础题渲染一个列表使用正确的 key实现一个虚拟列表处理 10000 条数据进阶题实现一个可拖拽排序的虚拟列表实现一个无限滚动的虚拟列表十、小结要点说明key唯一稳定帮助 React 识别变化索引作为 key仅在列表静态且不重新排序时使用虚拟列表大数据量使用 react-window性能优化React.memo、避免内联函数、overscanCount