固定高度的,虚拟卡片列表

  1. import { useEffect, useState, useMemo } from 'react';
  2. import { array, number } from 'prop-types';
  3. import { Card, Avatar } from 'antd';
  4. import { useResizeDetector } from 'react-resize-detector';
  5. import debounce from 'lodash.debounce';
  6. import styles from './style.module.less';
  7. const { Meta } = Card;
  8. VirtualCardList.propTypes = {
  9. dataSource: array.isRequired,
  10. itemSize: number, // 每个条目的高度
  11. bufferSize: number, // 缓冲的条数
  12. };
  13. VirtualCardList.defaultProps = {
  14. bufferSize: 0,
  15. };
  16. function VirtualCardList({ dataSource, itemSize, bufferSize }) {
  17. // 可视区的宽高
  18. const { height: clientHeight, ref } = useResizeDetector();
  19. // 开始 & 结束索引
  20. let [[startIndex, endIndex], setIndex] = useState([0, 0]);
  21. // 数据的长度
  22. const itemCount = useMemo(() => dataSource.length, [dataSource]);
  23. // 渲染可视区的数据
  24. const clientData = useMemo(() => {
  25. return dataSource
  26. .map((it, index) => ({ ...it, index }))
  27. .slice(startIndex, endIndex);
  28. }, [dataSource, startIndex, endIndex]);
  29. const offset = useMemo(() => startIndex * itemSize, [startIndex, itemSize]);
  30. // forwardRef.current = ref
  31. // 初始化时,event为 undefined,滚动时,才有 event对象
  32. const onScroll = debounce(e => {
  33. e = e ?? ref.current;
  34. if (!e || !clientHeight) return;
  35. const scrollTop = (e.target ?? e).scrollTop;
  36. // 显示条目的开始下标 = 容器滚动的高度 / 每条的高度
  37. startIndex = Math.floor(scrollTop / itemSize);
  38. // 结束下标 = 可视区的高度 / 每条的高度
  39. endIndex = startIndex + Math.floor(clientHeight / itemSize);
  40. // console.log('current', scrollTop, startIndex, endIndex);
  41. // 缓存的条数 2,前面缓存2个,后面缓存2个
  42. startIndex -= bufferSize;
  43. endIndex += bufferSize;
  44. // 如果开始索引小于0 ,就为0;如果结束索引大于数据的长度,就为数据的长度
  45. startIndex = startIndex <= 0 ? 0 : startIndex;
  46. endIndex = endIndex >= itemCount ? itemCount : endIndex;
  47. setIndex([startIndex, endIndex]);
  48. }, 10);
  49. useEffect(onScroll, [dataSource, clientHeight]);
  50. return (
  51. <div
  52. ref={ref}
  53. className={styles.clientHeight}
  54. onScroll={onScroll}
  55. >
  56. {/* 数据的条数乘以每个 itemSize,撑开 div的高度 */}
  57. <div
  58. className={styles.container}
  59. style={{ height: itemCount * itemSize }}
  60. >
  61. <div
  62. style={{
  63. transform: `translate3d(0,${offset}px,0)`,
  64. willChange: 'transform',
  65. }}
  66. >
  67. {
  68. clientData.map(it => {
  69. // console.log('it.index', it.index === startIndex)
  70. return (
  71. <Card
  72. pid={it.index}
  73. style={{ height: itemSize }}
  74. cover={<img alt='example' src={it.image} className={styles.img} />}
  75. >
  76. <Meta
  77. avatar={<Avatar src='https://joeschmoe.io/api/v1/random' />}
  78. title={it.title}
  79. description={it.desc}
  80. />
  81. </Card>
  82. );
  83. })
  84. }
  85. </div>
  86. </div>
  87. </div>
  88. );
  89. }
  90. export default VirtualCardList;

style.module.less

  1. .clientHeight {
  2. height: 433px;
  3. overflow-y: auto;
  4. background-color: rgba(153, 0, 255, 0.03);
  5. }
  6. .container {
  7. position: relative;
  8. width: 100%;
  9. .img {
  10. width: 100%;
  11. height: 120px;
  12. }
  13. }

使用 VirtualCardList

  1. import { VirtualCardList } from '@/components';
  2. function App() {
  3. return (
  4. <Skeleton
  5. active
  6. loading={false}
  7. paragraph={{rows: 12}}
  8. >
  9. <VirtualCardList
  10. dataSource={mockData}
  11. itemSize={200}
  12. bufferSize={2}
  13. />
  14. </Skeleton>
  15. )
  16. }