Appearance
虚拟列表(react-window-react-virtualized)
第一部分:虚拟列表概述
1.1 什么是虚拟列表
虚拟列表(Virtual List)是一种优化长列表渲染的技术。它只渲染可见区域的列表项,而不是渲染整个列表,从而大幅提升性能。
核心原理:
javascript
// 传统列表:渲染所有10000项
function TraditionalList({ items }) {
return (
<div>
{items.map(item => (
<div key={item.id}>{item.content}</div>
))}
</div>
);
// 性能问题:
// - DOM节点过多(10000个)
// - 内存占用大
// - 渲染时间长
// - 滚动卡顿
}
// 虚拟列表:只渲染可见项
function VirtualList({ items, height, itemHeight }) {
const [scrollTop, setScrollTop] = useState(0);
// 计算可见范围
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.ceil((scrollTop + height) / itemHeight);
// 只渲染可见项
const visibleItems = items.slice(startIndex, endIndex);
return (
<div
style={{ height, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight }}>
{visibleItems.map((item, i) => (
<div
key={item.id}
style={{
position: 'absolute',
top: (startIndex + i) * itemHeight,
height: itemHeight
}}
>
{item.content}
</div>
))}
</div>
</div>
);
// 优势:
// - DOM节点少(约20个)
// - 内存占用小
// - 渲染快速
// - 滚动流畅
}1.2 为什么需要虚拟列表
javascript
// 性能对比
const ITEMS_COUNT = 10000;
// 场景1:渲染10000个简单列表项
function NonVirtualized() {
const items = Array.from({ length: ITEMS_COUNT }, (_, i) => ({
id: i,
text: `Item ${i}`
}));
return (
<div style={{ height: '500px', overflow: 'auto' }}>
{items.map(item => (
<div key={item.id} style={{ height: '50px' }}>
{item.text}
</div>
))}
</div>
);
// 性能指标:
// - 首次渲染:~2000ms
// - DOM节点:10000个
// - 内存:~50MB
// - 滚动FPS:~20fps(卡顿)
}
// 场景2:使用虚拟列表
import { FixedSizeList } from 'react-window';
function Virtualized() {
const items = Array.from({ length: ITEMS_COUNT }, (_, i) => ({
id: i,
text: `Item ${i}`
}));
const Row = ({ index, style }) => (
<div style={style}>{items[index].text}</div>
);
return (
<FixedSizeList
height={500}
itemCount={ITEMS_COUNT}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
// 性能指标:
// - 首次渲染:~50ms(40倍提升)
// - DOM节点:~20个
// - 内存:~2MB(25倍减少)
// - 滚动FPS:~60fps(流畅)
}1.3 主流虚拟列表库对比
javascript
// 1. react-window(推荐)
// - 体积小(6KB)
// - 性能好
// - API简单
// - 维护活跃
// 2. react-virtualized
// - 功能丰富
// - 体积大(30KB)
// - API复杂
// - 成熟稳定
// 3. 选择建议
const recommendation = {
'react-window': '新项目、简单场景',
'react-virtualized': '复杂场景、需要高级功能'
};
// react-window示例
import { FixedSizeList } from 'react-window';
function SimpleList() {
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
return (
<FixedSizeList
height={400}
itemCount={1000}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// react-virtualized示例
import { List } from 'react-virtualized';
function SimpleListVirtualized() {
const rowRenderer = ({ index, key, style }) => (
<div key={key} style={style}>
Row {index}
</div>
);
return (
<List
width={300}
height={400}
rowCount={1000}
rowHeight={35}
rowRenderer={rowRenderer}
/>
);
}第二部分:react-window详解
2.1 FixedSizeList固定高度列表
javascript
import { FixedSizeList } from 'react-window';
// 基础用法
function BasicFixedList() {
const items = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
const Row = ({ index, style }) => (
<div style={style}>
{items[index]}
</div>
);
return (
<FixedSizeList
height={400} // 列表高度
itemCount={1000} // 总项数
itemSize={35} // 每项高度
width="100%" // 列表宽度
>
{Row}
</FixedSizeList>
);
}
// 带样式的列表
function StyledList() {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
title: `Title ${i}`,
subtitle: `Subtitle ${i}`
}));
const Row = ({ index, style }) => {
const item = items[index];
return (
<div
style={{
...style,
display: 'flex',
alignItems: 'center',
padding: '0 16px',
borderBottom: '1px solid #eee'
}}
>
<div>
<div style={{ fontWeight: 'bold' }}>{item.title}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{item.subtitle}
</div>
</div>
</div>
);
};
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={60}
width={400}
>
{Row}
</FixedSizeList>
);
}
// 响应式列表
function ResponsiveList() {
const containerRef = useRef(null);
const [width, setWidth] = useState(0);
useEffect(() => {
const updateWidth = () => {
if (containerRef.current) {
setWidth(containerRef.current.offsetWidth);
}
};
updateWidth();
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
}, []);
const Row = ({ index, style }) => (
<div style={style}>Item {index}</div>
);
return (
<div ref={containerRef} style={{ width: '100%' }}>
{width > 0 && (
<FixedSizeList
height={500}
itemCount={1000}
itemSize={35}
width={width}
>
{Row}
</FixedSizeList>
)}
</div>
);
}
// 带交互的列表
function InteractiveList() {
const [selectedIndex, setSelectedIndex] = useState(null);
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
const Row = ({ index, style }) => {
const item = items[index];
const isSelected = selectedIndex === index;
return (
<div
style={{
...style,
backgroundColor: isSelected ? '#e3f2fd' : 'white',
cursor: 'pointer',
padding: '0 16px',
display: 'flex',
alignItems: 'center'
}}
onClick={() => setSelectedIndex(index)}
>
{item.name}
</div>
);
};
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={40}
width={300}
>
{Row}
</FixedSizeList>
);
}2.2 VariableSizeList动态高度列表
javascript
import { VariableSizeList } from 'react-window';
// 基础动态高度
function BasicVariableList() {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
content: `Item ${i}`,
height: 40 + Math.floor(Math.random() * 60) // 40-100px随机高度
}));
const listRef = useRef(null);
const getItemSize = (index) => items[index].height;
const Row = ({ index, style }) => (
<div style={style}>
{items[index].content} (Height: {items[index].height}px)
</div>
);
return (
<VariableSizeList
ref={listRef}
height={400}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
// 展开/折叠列表
function ExpandableList() {
const [expandedItems, setExpandedItems] = useState(new Set());
const items = Array.from({ length: 100 }, (_, i) => ({
id: i,
title: `Item ${i}`,
details: `Details for item ${i}...`.repeat(5)
}));
const listRef = useRef(null);
const toggleExpand = (index) => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
return newSet;
});
// 重新计算尺寸
if (listRef.current) {
listRef.current.resetAfterIndex(index);
}
};
const getItemSize = (index) => {
return expandedItems.has(index) ? 150 : 50;
};
const Row = ({ index, style }) => {
const item = items[index];
const isExpanded = expandedItems.has(index);
return (
<div
style={{
...style,
borderBottom: '1px solid #ddd',
padding: '8px'
}}
>
<div
onClick={() => toggleExpand(index)}
style={{ cursor: 'pointer', fontWeight: 'bold' }}
>
{isExpanded ? '▼' : '▶'} {item.title}
</div>
{isExpanded && (
<div style={{ marginTop: '8px', color: '#666' }}>
{item.details}
</div>
)}
</div>
);
};
return (
<VariableSizeList
ref={listRef}
height={500}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
// 自动测量高度
function AutoSizedList() {
const rowHeights = useRef({});
const listRef = useRef(null);
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
content: `Content for item ${i}`.repeat(Math.floor(Math.random() * 5) + 1)
}));
const setRowHeight = (index, size) => {
if (rowHeights.current[index] !== size) {
rowHeights.current[index] = size;
if (listRef.current) {
listRef.current.resetAfterIndex(index);
}
}
};
const getRowHeight = (index) => {
return rowHeights.current[index] || 100; // 默认高度
};
const Row = ({ index, style }) => {
const rowRef = useRef(null);
useEffect(() => {
if (rowRef.current) {
setRowHeight(index, rowRef.current.clientHeight);
}
});
return (
<div style={style}>
<div
ref={rowRef}
style={{ padding: '16px', borderBottom: '1px solid #eee' }}
>
{items[index].content}
</div>
</div>
);
};
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={items.length}
itemSize={getRowHeight}
width="100%"
>
{Row}
</VariableSizeList>
);
}2.3 FixedSizeGrid固定网格
javascript
import { FixedSizeGrid } from 'react-window';
// 基础网格
function BasicGrid() {
const Cell = ({ columnIndex, rowIndex, style }) => (
<div
style={{
...style,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #ddd'
}}
>
{rowIndex},{columnIndex}
</div>
);
return (
<FixedSizeGrid
columnCount={1000}
columnWidth={100}
height={600}
rowCount={1000}
rowHeight={35}
width={800}
>
{Cell}
</FixedSizeGrid>
);
}
// 图片网格
function ImageGrid() {
const images = Array.from({ length: 1000 }, (_, i) => ({
id: i,
url: `https://picsum.photos/200/200?random=${i}`,
title: `Image ${i}`
}));
const COLUMN_COUNT = 4;
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * COLUMN_COUNT + columnIndex;
const image = images[index];
if (!image) return null;
return (
<div
style={{
...style,
padding: '8px'
}}
>
<div
style={{
width: '100%',
height: '100%',
backgroundColor: '#f5f5f5',
borderRadius: '8px',
overflow: 'hidden'
}}
>
<img
src={image.url}
alt={image.title}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/>
</div>
</div>
);
};
return (
<FixedSizeGrid
columnCount={COLUMN_COUNT}
columnWidth={200}
height={600}
rowCount={Math.ceil(images.length / COLUMN_COUNT)}
rowHeight={200}
width={800}
>
{Cell}
</FixedSizeGrid>
);
}
// 响应式网格
function ResponsiveGrid() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const containerRef = useRef(null);
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight
});
}
};
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => window.removeEventListener('resize', updateDimensions);
}, []);
const COLUMN_WIDTH = 150;
const columnCount = Math.floor(dimensions.width / COLUMN_WIDTH);
const Cell = ({ columnIndex, rowIndex, style }) => (
<div style={{ ...style, border: '1px solid #ddd' }}>
Cell {rowIndex},{columnIndex}
</div>
);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '600px' }}
>
{dimensions.width > 0 && (
<FixedSizeGrid
columnCount={columnCount}
columnWidth={COLUMN_WIDTH}
height={dimensions.height}
rowCount={100}
rowHeight={150}
width={dimensions.width}
>
{Cell}
</FixedSizeGrid>
)}
</div>
);
}2.4 高级特性
javascript
// 1. 滚动到指定位置
function ScrollToItem() {
const listRef = useRef(null);
const [targetIndex, setTargetIndex] = useState('');
const scrollToItem = () => {
const index = parseInt(targetIndex);
if (listRef.current && !isNaN(index)) {
listRef.current.scrollToItem(index, 'center');
}
};
const Row = ({ index, style }) => (
<div style={style}>Item {index}</div>
);
return (
<div>
<div style={{ marginBottom: '16px' }}>
<input
type="number"
value={targetIndex}
onChange={e => setTargetIndex(e.target.value)}
placeholder="输入索引"
/>
<button onClick={scrollToItem}>滚动到</button>
</div>
<FixedSizeList
ref={listRef}
height={400}
itemCount={1000}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
</div>
);
}
// 2. 监听滚动事件
function ScrollListener() {
const [scrollInfo, setScrollInfo] = useState({
scrollDirection: 'forward',
scrollOffset: 0,
scrollUpdateWasRequested: false
});
const handleScroll = ({
scrollDirection,
scrollOffset,
scrollUpdateWasRequested
}) => {
setScrollInfo({
scrollDirection,
scrollOffset,
scrollUpdateWasRequested
});
};
const Row = ({ index, style }) => (
<div style={style}>Item {index}</div>
);
return (
<div>
<div style={{ marginBottom: '16px' }}>
<div>方向: {scrollInfo.scrollDirection}</div>
<div>偏移: {scrollInfo.scrollOffset}px</div>
</div>
<FixedSizeList
height={400}
itemCount={1000}
itemSize={35}
width="100%"
onScroll={handleScroll}
>
{Row}
</FixedSizeList>
</div>
);
}
// 3. 动态加载数据
function InfiniteLoader() {
const [items, setItems] = useState(
Array.from({ length: 50 }, (_, i) => `Item ${i}`)
);
const [isLoading, setIsLoading] = useState(false);
const listRef = useRef(null);
const loadMore = useCallback(async () => {
if (isLoading) return;
setIsLoading(true);
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 1000));
setItems(prev => [
...prev,
...Array.from({ length: 50 }, (_, i) =>
`Item ${prev.length + i}`
)
]);
setIsLoading(false);
}, [isLoading]);
const handleScroll = ({ scrollDirection, scrollOffset }) => {
const listHeight = 400;
const totalHeight = items.length * 35;
// 接近底部时加载更多
if (scrollDirection === 'forward' &&
scrollOffset + listHeight > totalHeight - 200) {
loadMore();
}
};
const Row = ({ index, style }) => {
if (index === items.length) {
return (
<div style={{ ...style, textAlign: 'center' }}>
{isLoading ? 'Loading...' : 'End'}
</div>
);
}
return <div style={style}>{items[index]}</div>;
};
return (
<FixedSizeList
ref={listRef}
height={400}
itemCount={items.length + 1}
itemSize={35}
width="100%"
onScroll={handleScroll}
>
{Row}
</FixedSizeList>
);
}
// 4. 缓存优化
import memoize from 'memoize-one';
function OptimizedList() {
const [items] = useState(
Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random()
}))
);
// 缓存行数据创建
const createItemData = memoize((items) => ({ items }));
const itemData = createItemData(items);
const Row = memo(({ index, style, data }) => {
const item = data.items[index];
return (
<div style={style}>
{item.name}: {item.value.toFixed(2)}
</div>
);
});
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
width="100%"
itemData={itemData}
>
{Row}
</FixedSizeList>
);
}第三部分:react-virtualized详解
3.1 List组件
javascript
import { List, AutoSizer } from 'react-virtualized';
// 基础List
function BasicListVirtualized() {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
const rowRenderer = ({ index, key, style }) => (
<div key={key} style={style}>
{items[index].name}
</div>
);
return (
<List
width={300}
height={400}
rowCount={items.length}
rowHeight={35}
rowRenderer={rowRenderer}
/>
);
}
// AutoSizer自动尺寸
function AutoSizedList() {
const rowRenderer = ({ index, key, style }) => (
<div key={key} style={style}>
Row {index}
</div>
);
return (
<div style={{ width: '100%', height: '400px' }}>
<AutoSizer>
{({ width, height }) => (
<List
width={width}
height={height}
rowCount={1000}
rowHeight={35}
rowRenderer={rowRenderer}
/>
)}
</AutoSizer>
</div>
);
}
// CellMeasurer动态高度
import { List, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
function DynamicHeightList() {
const cache = useRef(
new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 100
})
);
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
content: `Content ${i}`.repeat(Math.floor(Math.random() * 10) + 1)
}));
const rowRenderer = ({ index, key, parent, style }) => (
<CellMeasurer
cache={cache.current}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<div style={style}>
<div style={{ padding: '16px', borderBottom: '1px solid #ddd' }}>
{items[index].content}
</div>
</div>
</CellMeasurer>
);
return (
<List
width={400}
height={600}
rowCount={items.length}
deferredMeasurementCache={cache.current}
rowHeight={cache.current.rowHeight}
rowRenderer={rowRenderer}
/>
);
}3.2 Grid和Table
javascript
import { Grid, Table, Column } from 'react-virtualized';
// Grid网格
function VirtualizedGrid() {
const cellRenderer = ({ columnIndex, key, rowIndex, style }) => (
<div
key={key}
style={{
...style,
border: '1px solid #ddd',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{rowIndex},{columnIndex}
</div>
);
return (
<Grid
cellRenderer={cellRenderer}
columnCount={50}
columnWidth={100}
height={600}
rowCount={50}
rowHeight={100}
width={800}
/>
);
}
// Table表格
function VirtualizedTable() {
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Name ${i}`,
email: `email${i}@example.com`,
age: 20 + (i % 50)
}));
return (
<Table
width={800}
height={600}
headerHeight={40}
rowHeight={50}
rowCount={data.length}
rowGetter={({ index }) => data[index]}
>
<Column
label="ID"
dataKey="id"
width={100}
/>
<Column
label="Name"
dataKey="name"
width={200}
/>
<Column
label="Email"
dataKey="email"
width={300}
/>
<Column
label="Age"
dataKey="age"
width={100}
/>
</Table>
);
}
// 自定义单元格渲染
function CustomTable() {
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `User ${i}`,
status: i % 3 === 0 ? 'active' : 'inactive',
avatar: `https://i.pravatar.cc/150?img=${i}`
}));
const statusRenderer = ({ cellData }) => (
<span
style={{
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: cellData === 'active' ? '#4caf50' : '#f44336',
color: 'white'
}}
>
{cellData}
</span>
);
const avatarRenderer = ({ cellData }) => (
<img
src={cellData}
alt="avatar"
style={{ width: '32px', height: '32px', borderRadius: '50%' }}
/>
);
return (
<Table
width={800}
height={600}
headerHeight={50}
rowHeight={60}
rowCount={data.length}
rowGetter={({ index }) => data[index]}
>
<Column
label="Avatar"
dataKey="avatar"
width={80}
cellRenderer={avatarRenderer}
/>
<Column
label="Name"
dataKey="name"
width={200}
/>
<Column
label="Status"
dataKey="status"
width={150}
cellRenderer={statusRenderer}
/>
</Table>
);
}第四部分:实战应用
4.1 聊天消息列表
javascript
import { VariableSizeList } from 'react-window';
function ChatMessageList() {
const [messages, setMessages] = useState([]);
const listRef = useRef(null);
const messageHeights = useRef({});
// 添加新消息
const addMessage = useCallback((text) => {
setMessages(prev => [
...prev,
{
id: Date.now(),
text,
timestamp: new Date(),
user: 'me'
}
]);
// 滚动到底部
setTimeout(() => {
if (listRef.current) {
listRef.current.scrollToItem(messages.length, 'end');
}
}, 0);
}, [messages.length]);
const setMessageHeight = (index, height) => {
if (messageHeights.current[index] !== height) {
messageHeights.current[index] = height;
if (listRef.current) {
listRef.current.resetAfterIndex(index);
}
}
};
const getMessageHeight = (index) => {
return messageHeights.current[index] || 80;
};
const Message = ({ index, style }) => {
const message = messages[index];
const messageRef = useRef(null);
useEffect(() => {
if (messageRef.current) {
setMessageHeight(index, messageRef.current.clientHeight);
}
});
return (
<div style={style}>
<div
ref={messageRef}
style={{
padding: '8px 16px',
margin: '8px',
backgroundColor: message.user === 'me' ? '#e3f2fd' : '#f5f5f5',
borderRadius: '8px',
maxWidth: '70%',
marginLeft: message.user === 'me' ? 'auto' : '0'
}}
>
<div>{message.text}</div>
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
{message.timestamp.toLocaleTimeString()}
</div>
</div>
</div>
);
};
return (
<div>
<VariableSizeList
ref={listRef}
height={500}
itemCount={messages.length}
itemSize={getMessageHeight}
width="100%"
>
{Message}
</VariableSizeList>
<input
type="text"
onKeyPress={e => {
if (e.key === 'Enter' && e.target.value) {
addMessage(e.target.value);
e.target.value = '';
}
}}
placeholder="输入消息..."
style={{ width: '100%', padding: '12px' }}
/>
</div>
);
}4.2 电商商品列表
javascript
import { FixedSizeGrid } from 'react-window';
function ProductGrid() {
const [products] = useState(
Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: (Math.random() * 100).toFixed(2),
image: `https://picsum.photos/200/200?random=${i}`,
rating: (Math.random() * 5).toFixed(1)
}))
);
const COLUMN_COUNT = 4;
const COLUMN_WIDTH = 250;
const ROW_HEIGHT = 320;
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * COLUMN_COUNT + columnIndex;
const product = products[index];
if (!product) return null;
return (
<div style={{ ...style, padding: '8px' }}>
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
overflow: 'hidden',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<img
src={product.image}
alt={product.name}
style={{ width: '100%', height: '200px', objectFit: 'cover' }}
/>
<div style={{ padding: '12px', flex: 1 }}>
<h3 style={{ margin: '0 0 8px 0' }}>{product.name}</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '20px', fontWeight: 'bold', color: '#f44336' }}>
${product.price}
</span>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ color: '#ff9800' }}>★</span>
<span>{product.rating}</span>
</div>
</div>
<button
style={{
width: '100%',
marginTop: '12px',
padding: '8px',
backgroundColor: '#2196f3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Add to Cart
</button>
</div>
</div>
</div>
);
};
return (
<div style={{ width: '100%', height: '600px' }}>
<AutoSizer>
{({ width, height }) => {
const columnCount = Math.floor(width / COLUMN_WIDTH);
return (
<FixedSizeGrid
columnCount={columnCount}
columnWidth={COLUMN_WIDTH}
height={height}
rowCount={Math.ceil(products.length / columnCount)}
rowHeight={ROW_HEIGHT}
width={width}
>
{Cell}
</FixedSizeGrid>
);
}}
</AutoSizer>
</div>
);
}4.3 日志查看器
javascript
function LogViewer() {
const [logs] = useState(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
timestamp: new Date(Date.now() - (10000 - i) * 1000).toISOString(),
level: ['INFO', 'WARNING', 'ERROR'][Math.floor(Math.random() * 3)],
message: `Log message ${i}: ${Math.random().toString(36).substring(7)}`
}))
);
const [filter, setFilter] = useState('ALL');
const filteredLogs = useMemo(() => {
if (filter === 'ALL') return logs;
return logs.filter(log => log.level === filter);
}, [logs, filter]);
const getLevelColor = (level) => {
switch (level) {
case 'INFO': return '#2196f3';
case 'WARNING': return '#ff9800';
case 'ERROR': return '#f44336';
default: return '#666';
}
};
const Row = ({ index, style }) => {
const log = filteredLogs[index];
return (
<div
style={{
...style,
fontFamily: 'monospace',
fontSize: '12px',
padding: '4px 8px',
borderBottom: '1px solid #eee',
display: 'flex',
alignItems: 'center'
}}
>
<span style={{ color: '#999', marginRight: '8px' }}>
{log.timestamp}
</span>
<span
style={{
color: getLevelColor(log.level),
fontWeight: 'bold',
marginRight: '8px',
minWidth: '80px'
}}
>
[{log.level}]
</span>
<span>{log.message}</span>
</div>
);
};
return (
<div>
<div style={{ marginBottom: '16px' }}>
<button onClick={() => setFilter('ALL')}>All</button>
<button onClick={() => setFilter('INFO')}>Info</button>
<button onClick={() => setFilter('WARNING')}>Warning</button>
<button onClick={() => setFilter('ERROR')}>Error</button>
<span style={{ marginLeft: '16px' }}>
Total: {filteredLogs.length}
</span>
</div>
<FixedSizeList
height={600}
itemCount={filteredLogs.length}
itemSize={30}
width="100%"
>
{Row}
</FixedSizeList>
</div>
);
}注意事项
1. 性能优化
javascript
// ✅ 优化:使用memo
const Row = memo(({ index, style, data }) => {
const item = data[index];
return <div style={style}>{item.name}</div>;
});
// ✅ 优化:缓存itemData
const itemData = useMemo(() => ({ items }), [items]);
// ❌ 避免:在render中创建函数
function Bad() {
return (
<FixedSizeList
itemData={items} // ❌ 每次都是新对象
/>
);
}2. 滚动优化
javascript
// overscan提前渲染
<FixedSizeList
overscanCount={5} // 额外渲染5项
/>
// 平滑滚动
<FixedSizeList
useIsScrolling // 滚动时优化渲染
/>3. 内存管理
javascript
// 及时清理
useEffect(() => {
return () => {
if (listRef.current) {
listRef.current.scrollTo(0);
}
};
}, []);常见问题
Q1: react-window和react-virtualized选哪个?
A: 新项目优先react-window,复杂场景用react-virtualized。
Q2: 如何处理动态高度?
A: 使用VariableSizeList和CellMeasurer。
Q3: 虚拟列表支持横向滚动吗?
A: 支持,使用direction="horizontal"。
Q4: 如何优化首屏渲染?
A: 设置合理的overscanCount,预渲染部分内容。
Q5: 虚拟列表影响SEO吗?
A: 会影响,需要SSR配合或其他SEO策略。
Q6: 如何实现sticky header?
A: 使用react-virtualized的MultiGrid或自定义实现。
Q7: 虚拟列表支持拖拽吗?
A: 支持,但需要额外处理滚动和重新计算位置。
Q8: 如何调试虚拟列表?
A: 使用React DevTools和浏览器Performance工具。
Q9: 虚拟列表会导致内存泄漏吗?
A: 正确使用不会,注意清理ref和事件监听。
Q10: 如何测试虚拟列表组件?
A: 使用@testing-library/react模拟滚动事件。
总结
核心要点
1. 虚拟列表优势
✅ DOM节点少
✅ 内存占用小
✅ 渲染性能好
✅ 滚动流畅
2. 选择建议
✅ react-window: 简单场景
✅ react-virtualized: 复杂场景
✅ 自定义: 特殊需求
3. 最佳实践
✅ 合理的overscan
✅ memo优化渲染
✅ 缓存itemData
✅ 正确处理动态高度应用场景
1. 大数据列表
✅ 聊天记录
✅ 日志查看
✅ 商品列表
✅ 数据表格
2. 图片网格
✅ 相册
✅ 商品展示
✅ 瀑布流
3. 复杂数据
✅ 动态高度
✅ 嵌套列表
✅ 可展开项虚拟列表是处理大数据列表的最佳方案,合理使用能显著提升应用性能。