下面,我们来系统的梳理关于 React 虚拟化列表:react-window 的基本知识点:
一、虚拟化列表核心概念
1.1 什么是虚拟化列表?
虚拟化列表(也称为窗口化)是一种只渲染当前可见区域列表项的技术,而不是一次性渲染整个列表。通过仅渲染用户视口内的元素,可以大幅提升大型列表的性能。
1.2 为什么需要虚拟化列表?
- 性能提升:减少 DOM 节点数量,提高渲染效率
- 内存优化:避免加载所有数据到内存中
- 流畅滚动:确保大型列表的平滑滚动体验
- 快速响应:提升应用整体响应速度
1.3 虚拟化原理
┌───────────────────────┐
│ 可视区域 (Viewport) │
│ ┌───────────────────┐ │
│ │ 可见项 1 │ │
│ │ 可见项 2 │ │
│ │ 可见项 3 │ │
│ └───────────────────┘ │
│ │
│ 不可见项(不渲染) │
└───────────────────────┘
二、react-window 核心组件
2.1 固定高度列表 (FixedSizeList)
适用于所有项目高度相同的列表
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>第 {index} 行</div>
);
const MyList = () => (
<List
height={600} // 列表可视高度
width={300} // 列表宽度
itemCount={1000} // 总项目数
itemSize={50} // 每个项目高度
>
{Row}
</List>
);
2.2 可变高度列表 (VariableSizeList)
适用于项目高度不同的列表
import { VariableSizeList as List } from 'react-window';
const rowHeights = new Array(1000)
.fill(true)
.map(() => 25 + Math.round(Math.random() * 50));
const Row = ({ index, style }) => (
<div style={style}>第 {index} 行,高度: {rowHeights[index]}px</div>
);
const MyList = () => (
<List
height={600}
width={300}
itemCount={1000}
itemSize={index => rowHeights[index]} // 动态高度函数
estimatedItemSize={50} // 预估高度,用于滚动条计算
>
{Row}
</List>
);
2.3 固定尺寸网格 (FixedSizeGrid)
适用于固定尺寸的网格布局
import { FixedSizeGrid as Grid } from 'react-window';
const Cell = ({ columnIndex, rowIndex, style }) => (
<div style={style}>
行 {rowIndex}, 列 {columnIndex}
</div>
);
const MyGrid = () => (
<Grid
height={600}
width={900}
columnCount={10} // 总列数
rowCount={1000} // 总行数
columnWidth={90} // 列宽
rowHeight={50} // 行高
>
{Cell}
</Grid>
);
2.4 可变尺寸网格 (VariableSizeGrid)
适用于动态尺寸的网格布局
import { VariableSizeGrid as Grid } from 'react-window';
const columnWidths = new Array(10).fill(true).map(() => 75 + Math.round(Math.random() * 50));
const rowHeights = new Array(1000).fill(true).map(() => 25 + Math.round(Math.random() * 50));
const Cell = ({ columnIndex, rowIndex, style }) => (
<div style={style}>
{rowIndex},{columnIndex}
</div>
);
const MyGrid = () => (
<Grid
height={600}
width={900}
columnCount={10}
rowCount={1000}
columnWidth={index => columnWidths[index]}
rowHeight={index => rowHeights[index]}
estimatedColumnWidth={100}
estimatedRowHeight={50}
>
{Cell}
</Grid>
);
三、核心特性与高级用法
3.1 滚动控制
import { useRef } from 'react';
function MyList() {
const listRef = useRef();
const scrollToRow200 = () => {
listRef.current.scrollToItem(200);
};
const scrollToCenter = () => {
listRef.current.scrollToItem(300, "center");
};
return (
<>
<button onClick={scrollToRow200}>滚动到第200项</button>
<button onClick={scrollToCenter}>滚动到第300项(居中)</button>
<FixedSizeList
ref={listRef}
height={600}
width={300}
itemCount={1000}
itemSize={50}
>
{Row}
</FixedSizeList>
</>
);
}
3.2 无限滚动加载
import { useState, useCallback } from 'react';
const PAGE_SIZE = 20;
function InfiniteList() {
const [items, setItems] = useState(Array(100).fill().map((_, i) => `项目 ${i + 1}`));
const [isLoading, setIsLoading] = useState(false);
const loadMoreItems = useCallback(() => {
if (isLoading) return;
setIsLoading(true);
// 模拟API请求
setTimeout(() => {
const newItems = Array(PAGE_SIZE).fill().map(
(_, i) => `项目 ${items.length + i + 1}`
);
setItems(prev => [...prev, ...newItems]);
setIsLoading(false);
}, 1000);
}, [isLoading, items.length]);
const Row = useCallback(({ index, style }) => {
if (index >= items.length) {
return (
<div style={style}>
<button onClick={loadMoreItems}>加载更多</button>
</div>
);
}
return (
<div style={style}>
{items[index]}
</div>
);
}, [items, loadMoreItems]);
return (
<FixedSizeList
height={600}
width={300}
itemCount={items.length + 1} // 额外一项用于"加载更多"
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
3.3 动态尺寸调整
function DynamicSizeList() {
const listRef = useRef();
const [rowHeights, setRowHeights] = useState({});
// 更新项目高度
const setRowHeight = (index, height) => {
listRef.current.resetAfterIndex(0);
setRowHeights(prev => ({
...prev,
[index]: height
}));
};
const Row = ({ index, style }) => (
<DynamicRow
index={index}
style={style}
setHeight={setRowHeight}
/>
);
const getItemSize = (index) => rowHeights[index] || 50;
return (
<VariableSizeList
ref={listRef}
height={600}
width={300}
itemCount={1000}
itemSize={getItemSize}
estimatedItemSize={50}
>
{Row}
</VariableSizeList>
);
}
// 动态高度行组件
function DynamicRow({ index, style, setHeight }) {
const rowRef = useRef();
useEffect(() => {
if (rowRef.current) {
setHeight(index, rowRef.current.clientHeight);
}
}, [index, setHeight]);
return (
<div ref={rowRef} style={style}>
{/* 动态内容 */}
项目 {index}
<div style={{ height: 30 + (index % 10) * 5 }}>动态高度内容</div>
</div>
);
}
四、性能优化技巧
4.1 使用 PureComponent 或 React.memo
// 使用 React.memo 避免不必要的行渲染
const Row = React.memo(({ index, style }) => {
return (
<div style={style}>
项目 {index}
</div>
);
});
// 使用 shouldComponentUpdate
class RowClass extends React.PureComponent {
render() {
const { index, style } = this.props;
return <div style={style}>项目 {index}</div>;
}
}
4.2 避免内联函数
// 错误 ❌:每次渲染创建新函数
<FixedSizeList>
{({ index, style }) => <div style={style}>项目 {index}</div>}
</FixedSizeList>
// 正确 ✅:缓存行组件
const Row = React.useCallback(({ index, style }) => (
<div style={style}>项目 {index}</div>
), []);
<FixedSizeList>
{Row}
</FixedSizeList>
4.3 合理使用 estimatedItemSize
对于可变尺寸列表,提供准确的预估尺寸可以优化滚动条精度:
<VariableSizeList
estimatedItemSize={100} // 预估项目尺寸
// ...其他属性
/>
五、最佳实践与常见场景
5.1 大型数据列表
import { FixedSizeList } from 'react-window';
const BigList = ({ data }) => {
const Row = ({ index, style }) => (
<div style={style}>
<h3>{data[index].name}</h3>
<p>{data[index].description}</p>
</div>
);
return (
<FixedSizeList
height={600}
width={800}
itemCount={data.length}
itemSize={120}
>
{Row}
</FixedSizeList>
);
};
5.2 表格渲染
import { FixedSizeGrid } from 'react-window';
const DataTable = ({ columns, data }) => {
const Cell = ({ columnIndex, rowIndex, style }) => (
<div
style={{
...style,
padding: '8px',
borderBottom: '1px solid #eee',
display: 'flex',
alignItems: 'center'
}}
>
{data[rowIndex][columns[columnIndex].key]}
</div>
);
return (
<FixedSizeGrid
height={600}
width={1000}
columnCount={columns.length}
rowCount={data.length}
columnWidth={150}
rowHeight={50}
>
{Cell}
</FixedSizeGrid>
);
};
5.3 图片画廊
import { VariableSizeGrid } from 'react-window';
const ImageGallery = ({ images }) => {
// 计算列宽和行高
const columnWidth = 150;
const rowHeight = 150;
const columnCount = Math.floor(800 / columnWidth);
const rowCount = Math.ceil(images.length / columnCount);
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * columnCount + columnIndex;
if (index >= images.length) return null;
return (
<div style={style}>
<img
src={images[index].thumbnail}
alt={images[index].title}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
);
};
return (
<VariableSizeGrid
height={600}
width={800}
columnCount={columnCount}
rowCount={rowCount}
columnWidth={() => columnWidth}
rowHeight={() => rowHeight}
>
{Cell}
</VariableSizeGrid>
);
};
六、常见问题与解决方案
6.1 滚动位置重置问题
问题:列表数据更新后滚动位置重置
解决方案:使用 key
属性保持列表稳定
<FixedSizeList
key={stableListId} // 当stableListId不变时保持滚动位置
// ...其他属性
/>
6.2 滚动条跳动问题
问题:可变尺寸列表滚动条位置不准确
解决方案:
- 提供准确的
estimatedItemSize
- 使用
resetAfterIndex
方法更新尺寸缓存
const listRef = useRef();
// 当项目尺寸变化时
useEffect(() => {
listRef.current.resetAfterIndex(0);
}, [sizeData]);
6.3 内存泄漏问题
问题:卸载组件后仍有事件监听
解决方案:使用 ref 清理函数
useEffect(() => {
const listElement = listRef.current;
return () => {
listElement?.removeEventListener('scroll', handleScroll);
};
}, []);
七、与其他库对比
特性 | react-window | react-virtualized | @tanstack/react-virtual |
---|---|---|---|
包大小 | ~5kB | ~15kB | ~3kB |
性能 | 优秀 | 优秀 | 优秀 |
维护性 | 活跃维护 | 维护较少 | 活跃维护 |
功能丰富度 | 核心功能 | 功能全面 | 功能全面 |
学习曲线 | 简单 | 中等 | 中等 |
TypeScript支持 | 优秀 | 良好 | 优秀 |
文档质量 | 良好 | 优秀 | 优秀 |
八、案例:社交媒体动态流
import { VariableSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
const FeedList = ({ posts }) => {
// 测量每篇文章高度并缓存
const sizeMap = useRef({});
const listRef = useRef();
const setSize = useCallback((index, size) => {
sizeMap.current = { ...sizeMap.current, [index]: size };
listRef.current.resetAfterIndex(index);
}, []);
const getSize = (index) => sizeMap.current[index] || 100;
const Row = ({ index, style }) => (
<FeedItem
post={posts[index]}
style={style}
index={index}
setSize={setSize}
/>
);
return (
<AutoSizer>
{({ height, width }) => (
<VariableSizeList
ref={listRef}
height={height}
width={width}
itemCount={posts.length}
itemSize={getSize}
estimatedItemSize={300}
>
{Row}
</VariableSizeList>
)}
</AutoSizer>
);
};
const FeedItem = React.memo(({ post, style, index, setSize }) => {
const rowRef = useRef();
useEffect(() => {
if (rowRef.current) {
setSize(index, rowRef.current.clientHeight);
}
}, [index, setSize, post]);
return (
<div ref={rowRef} style={style}>
<div className="post-header">
<img src={post.user.avatar} alt={post.user.name} />
<h3>{post.user.name}</h3>
</div>
<p>{post.content}</p>
{post.image && (
<img
src={post.image}
alt="Post"
style={{ maxWidth: '100%' }}
/>
)}
<div className="post-actions">
<button>点赞</button>
<button>评论</button>
<button>分享</button>
</div>
</div>
);
});
九、总结
9.1 react-window 核心优势
- 轻量高效:极小的包体积,出色的性能
- 简单易用:直观的 API 设计
- 灵活性强:支持固定和可变尺寸
- 良好兼容:完美支持 React 18 新特性
- 功能完备:覆盖列表、网格等常见场景
9.2 实践总结
- 优先选择固定尺寸:当项目尺寸相同时使用 FixedSizeList
- 合理预估尺寸:对可变尺寸列表提供准确预估
- 避免内联函数:缓存行渲染函数
- 使用尺寸缓存:动态测量项目尺寸并缓存
- 配合 AutoSizer:自动适应容器尺寸
- 虚拟化一切可滚动内容:表格、网格、画廊等
9.3 性能优化矩阵
场景 | 推荐组件 | 优化技巧 |
---|---|---|
等高等宽列表 | FixedSizeList | 使用 React.memo 优化行组件 |
等高不等宽列表 | FixedSizeGrid | 精确计算列宽 |
动态高度列表 | VariableSizeList | 使用尺寸测量和缓存 |
大型表格 | FixedSizeGrid | 分区域渲染 |
瀑布流布局 | VariableSizeGrid | 动态计算行列尺寸 |