固定高度的,虚拟卡片列表
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 (<divref={ref}className={styles.clientHeight}onScroll={onScroll}>{/* 数据的条数乘以每个 itemSize,撑开 div的高度 */}<divclassName={styles.container}style={{ height: itemCount * itemSize }}><divstyle={{transform: `translate3d(0,${offset}px,0)`,willChange: 'transform',}}>{clientData.map(it => {// console.log('it.index', it.index === startIndex)return (<Cardpid={it.index}style={{ height: itemSize }}cover={<img alt='example' src={it.image} className={styles.img} />}><Metaavatar={<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 (<Skeletonactiveloading={false}paragraph={{rows: 12}}><VirtualCardListdataSource={mockData}itemSize={200}bufferSize={2}/></Skeleton>)}
