UNPKG

ahooks

Version:
117 lines (116 loc) 4.47 kB
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;