ahooks
Version:
react hooks library
117 lines (116 loc) • 4.47 kB
JavaScript
import { useEffect, useMemo, useState, useRef } from 'react';
import useEventListener from '../useEventListener';
import useLatest from '../useLatest';
import useMemoizedFn from '../useMemoizedFn';
import useSize from '../useSize';
import { getTargetElement } from '../utils/domTarget';
import { isNumber } from '../utils';
import useUpdateEffect from '../useUpdateEffect';
const useVirtualList = (list, options) => {
const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;
const itemHeightRef = useLatest(itemHeight);
const size = useSize(containerTarget);
const scrollTriggerByScrollToFunc = useRef(false);
const [targetList, setTargetList] = useState([]);
const [wrapperStyle, setWrapperStyle] = useState({});
const getVisibleCount = (containerHeight, fromIndex) => {
if (isNumber(itemHeightRef.current)) {
return Math.ceil(containerHeight / itemHeightRef.current);
}
let sum = 0;
let endIndex = 0;
for (let i = fromIndex; i < list.length; i++) {
const height = itemHeightRef.current(i, list[i]);
sum += height;
endIndex = i;
if (sum >= containerHeight) {
break;
}
}
return endIndex - fromIndex;
};
const getOffset = (scrollTop) => {
if (isNumber(itemHeightRef.current)) {
return Math.floor(scrollTop / itemHeightRef.current);
}
let sum = 0;
let offset = 0;
for (let i = 0; i < list.length; i++) {
const height = itemHeightRef.current(i, list[i]);
sum += height;
if (sum >= scrollTop) {
offset = i;
break;
}
}
return offset + 1;
};
// 获取上部高度
const getDistanceTop = (index) => {
if (isNumber(itemHeightRef.current)) {
const height = index * itemHeightRef.current;
return height;
}
const height = list
.slice(0, index)
.reduce((sum, _, i) => sum + itemHeightRef.current(i, list[i]), 0);
return height;
};
const totalHeight = useMemo(() => {
if (isNumber(itemHeightRef.current)) {
return list.length * itemHeightRef.current;
}
return list.reduce((sum, _, index) => sum + itemHeightRef.current(index, list[index]), 0);
}, [list]);
const calculateRange = () => {
const container = getTargetElement(containerTarget);
if (container) {
const { scrollTop, clientHeight } = container;
const offset = getOffset(scrollTop);
const visibleCount = getVisibleCount(clientHeight, offset);
const start = Math.max(0, offset - overscan);
const end = Math.min(list.length, offset + visibleCount + overscan);
const offsetTop = getDistanceTop(start);
setWrapperStyle({
height: totalHeight - offsetTop + 'px',
marginTop: offsetTop + 'px',
});
setTargetList(list.slice(start, end).map((ele, index) => ({
data: ele,
index: index + start,
})));
}
};
useUpdateEffect(() => {
const wrapper = getTargetElement(wrapperTarget);
if (wrapper) {
Object.keys(wrapperStyle).forEach((key) => (wrapper.style[key] = wrapperStyle[key]));
}
}, [wrapperStyle]);
useEffect(() => {
if (!(size === null || size === void 0 ? void 0 : size.width) || !(size === null || size === void 0 ? void 0 : size.height)) {
return;
}
calculateRange();
}, [size === null || size === void 0 ? void 0 : size.width, size === null || size === void 0 ? void 0 : size.height, list]);
useEventListener('scroll', (e) => {
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
calculateRange();
}, {
target: containerTarget,
});
const scrollTo = (index) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
return [targetList, useMemoizedFn(scrollTo)];
};
export default useVirtualList;