看了很多虚拟滚动的文章,各种解决办法,但我都觉得和自己心目中的虚拟滚动有些差距,于是在风和日丽的下午,自己动手实现一个组件级别的虚拟滚动。
组件适用场景
- 虚拟列表项高度不固定;
- 页面是自适应,导致列表项高度会产生变动;
- 列表存在滚动加载的场景;
组件实现
首先我们需要考虑以下几点:
-
虚拟滚动的判断条件
也就是在什么时候不渲染内部元素,在什么时候开始渲染内部元素,我首先考虑到的就是元素是否在视口可见。当元素在视口可见时,需要渲染内部元素,反之则不渲染,这里我们可以使用getBoundingClientRect,getBoundingClientRect是一个JavaScript方法,它返回一个包含当前元素相对于视口的位置和大小的DOMRect对象。我们可以通过判断它的top和bottom,监控当前列表项是否渲染。
-
在初始化没有确定高度的时候如何处理
我这里的解决方案是,初始化全部渲染,因为我是滚动加载,不需要考虑首次渲染卡顿问题,其他同学如果是一次性渲染,可以为组件添加默认值,默认前几个初始化渲染,让列表初始化有内容,后面的就可以由组件控制渲染了。
-
需要考虑视口大小变化的时候列表项高度变化的处理
这里的解决办法是监听窗口大小变化,重置高度为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
欢迎大家批评指正。