告别伪流式渲染:字符级状态机重塑AI对话富UI交互体验
2026/6/25 17:19:13
| 维度 | 内容 |
|---|---|
| What | 使用稳定的 key 标识列表项,使用虚拟列表处理大数据 |
| Why | 避免不必要的 DOM 操作,处理大量数据时的性能问题 |
| When | 动态列表、大数据量渲染(1000+ 条) |
| Where | 列表渲染组件中 |
| Who | 处理列表数据的开发者 |
| How | 使用唯一 ID 作为 key,使用 react-window 实现虚拟列表 |
key 是 React 用来识别列表项的唯一标识符,帮助 React 确定哪些元素被添加、更改或删除。
虚拟列表只渲染可视区域内的元素,滚动时动态替换,从而处理大量数据。
没有 key 时: [1,2,3] → [1,2,3,4] React 会重新渲染整个列表 有 key 时: [1,2,3] → [1,2,3,4] React 只添加新的第 4 项渲染 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} width="100%" > {Row} </List> ); }| 场景 | 优化方式 |
|---|---|
| 动态列表 | 稳定 key |
| 列表顺序可能变化 | 稳定 key(不要用索引) |
| 大数据量(1000+) | 虚拟列表 |
| 无限滚动 | 虚拟列表 + 懒加载 |
| 表格数据 | 虚拟表格 |
所有处理列表数据的 React 开发者。
// ✅ 好的 Key:使用数据中的唯一 ID <li key={todo.id}>{todo.text}</li> // ✅ 可以:使用索引(仅在列表静态且不会重新排序时) <li key={index}>{todo.text}</li> // ❌ 坏的 Key:使用随机数 <li key={Math.random()}>{todo.text}</li>| 场景 | 推荐 Key | 说明 |
|---|---|---|
| 数据库数据 | 数据库 ID | 最稳定 |
| 本地生成数据 | uuid / nanoid | 保证唯一性 |
| 静态列表 | 索引(谨慎) | 列表不变时可接受 |
| 动态列表 | 唯一标识符 | 必须稳定且唯一 |
// ❌ 当列表顺序变化时会有问题 {todos.map((todo, index) => ( <TodoItem key={index} todo={todo} /> ))} // 问题: // 1. 删除第一项时,所有后续项的 key 都改变了 // 2. React 会重新渲染所有项,而不是只删除一项 // 3. 可能导致组件状态错乱// 当没有唯一 ID 时,可以组合多个字段 {items.map(item => ( <div key={`${item.type}-${item.id}`}> {item.value} </div> ))}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} width="100%" itemData={items} > {Row} </List> ); }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} width="100%" > {Row} </List> ); }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> ); }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} width="100%" > {Row} </List> )} </InfiniteLoader> ); }import { FixedSizeList as List } from 'react-window'; function VirtualTable({ data }) { const Row = ({ index, style }) => { const row = data[index]; return ( <div style={style} className="table-row"> <div className="cell">{row.id}</div> <div className="cell">{row.name}</div> <div className="cell">{row.email}</div> </div> ); }; return ( <div> <div className="table-header"> <div className="cell">ID</div> <div className="cell">姓名</div> <div className="cell">邮箱</div> </div> <List height={400} itemCount={data.length} itemSize={35} width="100%" > {Row} </List> </div> ); }// 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行 // ... />// ❌ 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> ))}// ❌ 不能通过 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} /> ))}// ❌ 容器没有固定高度 <div> <List height="auto" ... /> {/* 不会工作 */} </div> // ✅ 容器必须有固定高度 <div style={{ height: '400px' }}> <List height={400} ... /> </div>| 要点 | 说明 |
|---|---|
| key | 唯一稳定,帮助 React 识别变化 |
| 索引作为 key | 仅在列表静态且不重新排序时使用 |
| 虚拟列表 | 大数据量使用 react-window |
| 性能优化 | React.memo、避免内联函数、overscanCount |