【React】突然想弄懂怎么写虚拟滚动列表

380 阅读1分钟

虚拟滚动:滚动的时候,列表只加载可视区区域内的数据,这和图片拦截的有点相似?

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>
  );
}

定义:可见区域进行滚动

image.png

具体实现:

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

参考: zhuanlan.zhihu.com/p/34585166