我们在使用电商平台的过程中,打开首页时,我们一直向下滑动就会有源源不断的推荐内容向我们展示。随着浏览页面操作越来越多,数据也越来越庞大,这类场景我们都可以统一称为长列表渲染。虚拟列表就是将驱动交给数据,通过区间来直接渲染区间内容中的数据DOM,解决了页面列表内元素过多操作卡顿的问题, 与数据加载无挂钩。
import { useEffect, useRef, useState, UIEvent, useMemo } from "react";
import Product from "./components/Product";
import "./styles.css";
/** @name 页面容器高度 */
const SCROLL_VIEW_HEIGHT: number = 500;
/** @name 列表项高度 */
const ITEM_HEIGHT: number = 100;
// qq
/** @name 预加载数量 */
const PRE_LOAD_COUNT: number = SCROLL_VIEW_HEIGHT / ITEM_HEIGHT;
export default function App() {
const [sourceData, setSourceData] = useState<number[]>([]);
const [showRange, setShowPageRange] = useState({
start: 0,
end: 10
});
/** 容器Ref */
const containerRef = useRef<HTMLDivElement | null>(null);
/**
* 创建列表显示数据
*/
const createListData = () => {
const initnalList: number[] = Array.from(Array(20).keys());
setSourceData(initnalList);
};
useEffect(() => {
createListData();
}, []);
/**
* 计算元素范围
*/
const calculateRange = () => {
const element = containerRef.current;
if (element) {
const offset: number = Math.floor(element.scrollTop / ITEM_HEIGHT) + 1;
console.log(offset, "offset");
const viewItemSize: number = Math.ceil(
element.clientHeight / ITEM_HEIGHT
);
const startSize: number = offset - PRE_LOAD_COUNT;
const endSize: number = viewItemSize + offset + PRE_LOAD_COUNT;
setShowPageRange({
start: startSize < 0 ? 0 : startSize,
end: endSize > sourceData.length ? sourceData.length : endSize
});
}
};
/**
* 计算当前是否已经到底底部
* @returns 是否到达底部
*/
const reachScrollBottom = (): boolean => {
const contentScrollTop = containerRef.current?.scrollTop || 0; //滚动条距离顶部
const clientHeight = containerRef.current?.clientHeight || 0; //可视区域
const scrollHeight = containerRef.current?.scrollHeight || 0; //滚动条内容的总高度
console.log(
scrollHeight,
clientHeight,
contentScrollTop,
"scrollContainer"
);
if (contentScrollTop + clientHeight >= scrollHeight) {
return true;
}
return false;
};
/**
* onScroll事件回调
* @param event { UIEvent<HTMLDivElement> } scrollview滚动参数
*/
const onContainerScroll = (event: UIEvent<HTMLDivElement>) => {
event.preventDefault();
if (reachScrollBottom()) {
setTimeout(() => {
let endIndex = showRange.end;
let pushData: number[] = [];
for (let index = 0; index < 20; index++) {
pushData.push(endIndex++);
}
setSourceData((arr) => {
return [...arr, ...pushData];
});
}, 0);
}
calculateRange();
};
/**
* 当前scrollView展示列表
*/
const currentViewList = useMemo(() => {
return sourceData
.slice(showRange.start, showRange.end)
.map((el, index) => ({
data: el,
index
}));
}, [showRange, sourceData]);
/**
* scrollView整体高度
*/
const scrollViewHeight = useMemo(() => {
return sourceData.length * ITEM_HEIGHT;
}, [sourceData]);
/**
* scrollView 偏移量
*/
const scrollViewOffset = useMemo(() => {
console.log(showRange.start, "showRange.start");
return showRange.start * ITEM_HEIGHT;
}, [showRange.start]);
return (
<div
ref={containerRef}
style={{
height: SCROLL_VIEW_HEIGHT,
overflow: "auto"
}}
className="scrollView"
onScroll={onContainerScroll}
>
<div
style={{
width: "100%",
height: scrollViewHeight - scrollViewOffset,
// transform: `translete3d(0,${scrollViewOffset},0)`,
marginTop: scrollViewOffset
}}
>
{currentViewList.map((e,index) => (
<Product key={e.data} index={index}/>
))}
</div>
</div>
);
}