固定高度的,虚拟卡片列表
import { useEffect, useState, useMemo } from 'react';
import { array, number } from 'prop-types';
import { Card, Avatar } from 'antd';
import { useResizeDetector } from 'react-resize-detector';
import debounce from 'lodash.debounce';
import styles from './style.module.less';
const { Meta } = Card;
VirtualCardList.propTypes = {
dataSource: array.isRequired,
itemSize: number, // 每个条目的高度
bufferSize: number, // 缓冲的条数
};
VirtualCardList.defaultProps = {
bufferSize: 0,
};
function VirtualCardList({ dataSource, itemSize, bufferSize }) {
// 可视区的宽高
const { height: clientHeight, ref } = useResizeDetector();
// 开始 & 结束索引
let [[startIndex, endIndex], setIndex] = useState([0, 0]);
// 数据的长度
const itemCount = useMemo(() => dataSource.length, [dataSource]);
// 渲染可视区的数据
const clientData = useMemo(() => {
return dataSource
.map((it, index) => ({ ...it, index }))
.slice(startIndex, endIndex);
}, [dataSource, startIndex, endIndex]);
const offset = useMemo(() => startIndex * itemSize, [startIndex, itemSize]);
// forwardRef.current = ref
// 初始化时,event为 undefined,滚动时,才有 event对象
const onScroll = debounce(e => {
e = e ?? ref.current;
if (!e || !clientHeight) return;
const scrollTop = (e.target ?? e).scrollTop;
// 显示条目的开始下标 = 容器滚动的高度 / 每条的高度
startIndex = Math.floor(scrollTop / itemSize);
// 结束下标 = 可视区的高度 / 每条的高度
endIndex = startIndex + Math.floor(clientHeight / itemSize);
// console.log('current', scrollTop, startIndex, endIndex);
// 缓存的条数 2,前面缓存2个,后面缓存2个
startIndex -= bufferSize;
endIndex += bufferSize;
// 如果开始索引小于0 ,就为0;如果结束索引大于数据的长度,就为数据的长度
startIndex = startIndex <= 0 ? 0 : startIndex;
endIndex = endIndex >= itemCount ? itemCount : endIndex;
setIndex([startIndex, endIndex]);
}, 10);
useEffect(onScroll, [dataSource, clientHeight]);
return (
<div
ref={ref}
className={styles.clientHeight}
onScroll={onScroll}
>
{/* 数据的条数乘以每个 itemSize,撑开 div的高度 */}
<div
className={styles.container}
style={{ height: itemCount * itemSize }}
>
<div
style={{
transform: `translate3d(0,${offset}px,0)`,
willChange: 'transform',
}}
>
{
clientData.map(it => {
// console.log('it.index', it.index === startIndex)
return (
<Card
pid={it.index}
style={{ height: itemSize }}
cover={<img alt='example' src={it.image} className={styles.img} />}
>
<Meta
avatar={<Avatar src='https://joeschmoe.io/api/v1/random' />}
title={it.title}
description={it.desc}
/>
</Card>
);
})
}
</div>
</div>
</div>
);
}
export default VirtualCardList;
style.module.less
.clientHeight {
height: 433px;
overflow-y: auto;
background-color: rgba(153, 0, 255, 0.03);
}
.container {
position: relative;
width: 100%;
.img {
width: 100%;
height: 120px;
}
}
使用 VirtualCardList
import { VirtualCardList } from '@/components';
function App() {
return (
<Skeleton
active
loading={false}
paragraph={{rows: 12}}
>
<VirtualCardList
dataSource={mockData}
itemSize={200}
bufferSize={2}
/>
</Skeleton>
)
}