基于react万能的虚拟滚动解决方案

1,694 阅读2分钟

看了很多虚拟滚动的文章,各种解决办法,但我都觉得和自己心目中的虚拟滚动有些差距,于是在风和日丽的下午,自己动手实现一个组件级别的虚拟滚动。

组件适用场景

  1. 虚拟列表项高度不固定;
  2. 页面是自适应,导致列表项高度会产生变动;
  3. 列表存在滚动加载的场景;

组件实现

首先我们需要考虑以下几点:

  1. 虚拟滚动的判断条件

    也就是在什么时候不渲染内部元素,在什么时候开始渲染内部元素,我首先考虑到的就是元素是否在视口可见。当元素在视口可见时,需要渲染内部元素,反之则不渲染,这里我们可以使用getBoundingClientRectgetBoundingClientRect是一个JavaScript方法,它返回一个包含当前元素相对于视口的位置和大小的DOMRect对象。我们可以通过判断它的top和bottom,监控当前列表项是否渲染。

  2. 在初始化没有确定高度的时候如何处理

    我这里的解决方案是,初始化全部渲染,因为我是滚动加载,不需要考虑首次渲染卡顿问题,其他同学如果是一次性渲染,可以为组件添加默认值,默认前几个初始化渲染,让列表初始化有内容,后面的就可以由组件控制渲染了。

  3. 需要考虑视口大小变化的时候列表项高度变化的处理

    这里的解决办法是监听窗口大小变化,重置高度为auto,并且强制刷新。

代码实现

import React, { useEffect, useRef, useState } from "react";
import { useUpdate } from "ahooks";
import { bindHandleScroll, removeScroll } from "@/utils/elementUtils";
import style from "./virtuallyItem.module.css";

const VirtuallyItem = (props) => {
  const update = useUpdate();
  // 用于记录当前元素的高度
  const itemHeight = useRef<number | null>(null);
  // 用户保存当前的元素
  const item = useRef<any>(null);
  // 判断当前元素是否在可视窗口
  const [isVisual, setIsVisual] = useState<boolean>(true);

  const scrollCallback = () => {
    // get position relative to viewport
    const rect = item.current?.getBoundingClientRect();
    const distanceFromTop = rect.top;
    const distanceFromBottom = rect.bottom;
    // 可视区域高度
    const viewportHeight =
      window.innerHeight || document.documentElement.clientHeight;
    if (
      (distanceFromTop > -200 && distanceFromTop < viewportHeight + 200) ||
      (distanceFromBottom > -200 && distanceFromBottom < viewportHeight + 200)
    ) {
      setIsVisual(true);
    } else {
      setIsVisual(false);
    }
  };

  const windowResize = () => {
    itemHeight.current = null;
    update();
  };

  useEffect(() => {
    bindHandleScroll(scrollCallback);
    window.addEventListener("resize", windowResize);

    return () => {
      removeScroll(scrollCallback);
      window.removeEventListener("resize", windowResize);
    };
  }, []);

  useEffect(() => {
    if (item.current && itemHeight.current !== item.current?.offsetHeight) {
      itemHeight.current = item.current?.offsetHeight;
    }
  }, [item.current, isVisual]);

  return (
    <div
      className={style.virtually_item}
      ref={item}
      style={{
        height: `${itemHeight.current ? `${itemHeight.current}px` : "auto"}`,
      }}
    >
      {isVisual && props.children}
    </div>
  );
};

export default VirtuallyItem;

bindHandleScroll是绑定滚动事件 removeScroll是移除滚动事件

在列表中使用

import React, { useEffect, useRef, useState } from "react";
import VirtuallyItem from "@/components/VirtuallyItem";

const ListBox = () => {
    const list = [.....];
    
    return <div>
        {list?.map(item => (
            <VirtuallyItem>
                {/* 原列表项dom */}
            </VirtuallyItem>
        )}
    </div>
}

export default ListBox;

结尾

以上就是我的简单实现啦,亲测还可以,目前用在我的博客 shimmer 欢迎大家批评指正😜。

原文链接 wp-boke.work/blog-detail…