虚拟滚动:滚动的时候,列表只加载可视区区域内的数据,这和图片拦截的有点相似?
antd design 的select 也有虚拟滚动的功能,翻了下select源码,基于一个叫rc-select的组件实现的, 然后它又基于rc-virtual-list组件实现
实现的过程比较复杂,在这里写一个简单的demo,慢慢上手领会。假设你有10000条数据加载, 总不可能一次性都加载吧
export default function VirtualListDemo() {
const data = Array.from({ length: 10000 }, (_, index) => `列表项 ${index}`);
return (
<div style={{ backgroundColor: "#ffff00" }}>
<VirtualList itemHeight={50} data={data} containerHeight={300} />
</div>
);
}
定义:可见区域进行滚动
具体实现:
1、计算可见区域的startIndex
2、计算可见区域的endIndex
3、计算startOffset对应的数据在整个列表中的偏移位置
先实现html布局,
- 相对定位列表元素list-view
- 绝对定位list-view-phantom,用来撑开列表
- 绝对定位list-view-content,列表可见元素
<div
className="list-view"
onScroll={calculateVisibleRange}
style={{ height: `${containerHeight}px` }}
ref={listRef}
>
<div
className="list-view-phantom"
style={{
height: `${data.length * itemHeight}px`,
}}
></div> // 高度等于数据的长度*高度
<div className="list-view-content" ref={listContentRef}>
{visibleItems.map((item) => (
<div style={{ height: `${itemHeight}px` }}>{item}</div>
))}
</div>
</div>
.list-view {
overflow: auto;
position: relative;
border: 1px solid #aaa;
}
.list-view-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-view-content {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.list-view-item {
padding: 5px;
color: #666;
line-height: 30px;
box-sizing: border-box;
}
const listContentRef = useRef(null);
const listRef = useRef(null);
const [visibleItems, setVisibleItems] = useState([]);
const [startIndex, setStartIndex] = useState(0);
const [endIndex, setEndIndex] = useState(0);
useEffect(() => {
calculateVisibleRange();
}, []);
useEffect(() => {
renderVisibleItems();
listContentRef.current.style.webkitTransform = `translate3d(0, ${
startIndex * itemHeight
}px, 0)`;
}, [startIndex, endIndex]);
const calculateVisibleRange = () => {
const scrollTop = listRef?.current?.scrollTop;
const visibleCount = Math.ceil(containerHeight / itemHeight); // 可视区域内可以加载的数量, 向上取整
const startIndex = Math.floor(scrollTop / itemHeight); // 可视区域的第一个加载元素的下标,向下取整
const endIndex = startIndex + visibleCount; // 可视区域的最后一个加载元素的下标
setStartIndex(startIndex);
setEndIndex(endIndex);
};
const renderVisibleItems = () => {
const items = data.slice(startIndex, endIndex + 1).map((item, index) => (
<li key={startIndex + index} className="list-view-item">
{item}
</li>
));
setVisibleItems(items);
};
以上代码在线demo