基于react万能的虚拟滚动解决方案
前端|react
发布于2023-06-13 最近修改2023-06-13
894
0
Shimmer
看了很多虚拟滚动的文章,各种解决办法,但我都觉得和自己心目中的虚拟滚动有些差距,于是在风和日丽的下午,自己动手实现一个组件级别的虚拟滚动。

组件适用场景

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

组件实现

首先我们需要考虑以下几点:
  1. 虚拟滚动的判断条件
    也就是在什么时候不渲染内部元素,在什么时候开始渲染内部元素,我首先考虑到的就是元素是否在视口可见。当元素在视口可见时,需要渲染内部元素,反之则不渲染,这里我们可以使用getBoundingClientRectgetBoundingClientRect是一个JavaScript方法,它返回一个包含当前元素相对于视口的位置和大小的DOMRect对象。我们可以通过判断它的top和bottom,监控当前列表项是否渲染。
  2. 在初始化没有确定高度的时候如何处理
    我这里的解决方案是,初始化全部渲染,因为我是滚动加载,不需要考虑首次渲染卡顿问题,其他同学如果是一次性渲染,可以为组件添加默认值,默认前几个初始化渲染,让列表初始化有内容,后面的就可以由组件控制渲染了。
  3. 需要考虑视口大小变化的时候列表项高度变化的处理
    这里的解决办法是监听窗口大小变化,重置高度为auto,并且强制刷新。

代码实现

javascript
复制代码
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是移除滚动事件
在列表中使用
javascript
复制代码
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 欢迎大家批评指正。
目录