import { useEffect, useState, useMemo, useRef, MutableRefObject } from 'react';
import useSize from '../useSize';
export interface OptionType {
itemHeight: number | ((index: number) => number); // 行高度,静态高度可以直接写入像素值,动态高度可传入函数
overscan?: number; // 视区上、下额外展示的 dom 节点数量
}
export default <T = any>(list: T[], options: OptionType) => {
const containerRef = useRef<HTMLElement | null>();
const size = useSize(containerRef as MutableRefObject<HTMLElement>);
// 暂时禁止 cache
// const distanceCache = useRef<{ [key: number]: number }>({});
const [state, setState] = useState({ start: 0, end: 10 });
const { itemHeight, overscan = 5 } = options;
if (!itemHeight) {
console.warn('please enter a valid itemHeight');
}
/**
* @description: 计算当前容器可以容纳多少
* @param {number} containerHeight
* @return {*}
*/
const getViewCapacity = (containerHeight: number) => {
if (typeof itemHeight === 'number') {
return Math.ceil(containerHeight / itemHeight);
}
const { start = 0 } = state;
let sum = 0;
let capacity = 0;
for (let i = start; i < list.length; i++) {
const height = (itemHeight as (index: number) => number)(i);
sum += height;
if (sum >= containerHeight) {
capacity = i;
break;
}
}
return capacity - start;
};
/**
* @description: 当前位移到第几个元素
* @param {number} scrollTop
* @return {*}
*/
const getOffset = (scrollTop: number) => {
// 如果itemHeight传的数字 直接计算offset
if (typeof itemHeight === 'number') {
return Math.floor(scrollTop / itemHeight) + 1;
}
let sum = 0;
let offset = 0;
// 如果是function 先计算出height 再循环计算offset
for (let i = 0; i < list.length; i++) {
const height = (itemHeight as (index: number) => number)(i);
sum += height;
if (sum >= scrollTop) {
offset = i;
break;
}
}
return offset + 1;
};
/**
* @description: 计算当前渲染的元素start值与end值
* @param {*}
* @return {*}
*/
const calculateRange = () => {
const element = containerRef.current;
if (element) {
const offset = getOffset(element.scrollTop);
const viewCapacity = getViewCapacity(element.clientHeight);
const from = offset - overscan;
const to = offset + viewCapacity + overscan;
setState({
start: from < 0 ? 0 : from,
end: to > list.length ? list.length : to,
});
}
};
useEffect(() => {
calculateRange();
}, [size.width, size.height]);
/**
* @description: 计算总高度
* @param {*} useMemo
* @return {*}
*/
const totalHeight = useMemo(() => {
if (typeof itemHeight === 'number') {
return list.length * itemHeight;
}
return list.reduce((sum, _, index) => sum + itemHeight(index), 0);
}, [list.length]);
/**
* @description: 已经渲染过的元素的高度
* @param {number} index
* @return {*}
*/
const getDistanceTop = (index: number) => {
// 如果有缓存,优先返回缓存值
// if (enableCache && distanceCache.current[index]) {
// return distanceCache.current[index];
// }
if (typeof itemHeight === 'number') {
const height = index * itemHeight;
// if (enableCache) {
// distanceCache.current[index] = height;
// }
return height;
}
const height = list.slice(0, index).reduce((sum, _, i) => sum + itemHeight(i), 0);
// if (enableCache) {
// distanceCache.current[index] = height;
// }
return height;
};
/**
* @description: 快速滚动到指定 index
* @param {number} index
* @return {*}
*/
const scrollTo = (index: number) => {
if (containerRef.current) {
containerRef.current.scrollTop = getDistanceTop(index);
calculateRange();
}
};
/**
* @description: 距离外层包裹器的高度
* @param {*} useMemo
* @return {*}
*/
const offsetTop = useMemo(() => getDistanceTop(state.start), [state.start]);
return {
list: list.slice(state.start, state.end).map((ele, index) => ({
data: ele,
index: index + state.start,
})),
scrollTo,
containerProps: {
ref: (ele: any) => {
containerRef.current = ele;
},
onScroll: (e: any) => {
e.preventDefault();
calculateRange();
},
style: { overflowY: 'auto' as const },
},
wrapperProps: {
style: {
width: '100%',
height: totalHeight - offsetTop,
marginTop: offsetTop,
},
},
};
};
一二八、【源码阅读篇】ahooks -- useVirtualList(虚拟列表)
最新推荐文章于 2024-01-01 10:01:56 发布