本文是深入浅出 ahooks 源码系列文章的第十八篇,该系列已整顿成文档 - 地址。感觉还不错,给个 star 反对一下哈,Thanks。
简介
提供虚拟化列表能力的 Hook,用于解决展现海量数据渲染时首屏渲染迟缓和滚动卡顿问题。
详情可见官网,文章源代码能够点击这里。
实现原理
其实现原理监听内部容器的 scroll 事件以及其 size 发生变化的时候,触发计算逻辑算出外部容器的高度和 marginTop 值。
具体实现
其监听滚动逻辑如下:
// 当内部容器的 size 发生变化的时候,触发计算逻辑
useEffect(() => {if (!size?.width || !size?.height) {return;}
// 从新计算逻辑
calculateRange();}, [size?.width, size?.height, list]);
// 监听内部容器的 scroll 事件
useEventListener(
'scroll',
e => {
// 如果是间接跳转,则不须要从新计算
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
// 计算
calculateRange();},
{
// 内部容器
target: containerTarget,
},
);
其中 calculateRange 十分重要,它根本实现了虚构滚动的主流程逻辑,其次要做了以下的事件:
- 获取到整个外部容器的高度 totalHeight。
- 依据内部容器的 scrollTop 算出曾经“滚过”多少项,值为 offset。
- 依据内部容器高度以及以后的开始索引,获取到内部容器能承载的个数 visibleCount。
- 并依据 overscan(视区上、下额定展现的 DOM 节点数量)计算出开始索引(start)和(end)。
- 依据开始索引获取到其间隔最开始的间隔(offsetTop)。
- 最初依据 offsetTop 和 totalHeight 设置外部容器的高度和 marginTop 值。
变量很多,能够联合下图,会比拟清晰了解:
<img width=”809″ alt=”image” src=”https://user-images.githubusercontent.com/20135760/185148673-1b02da0a-c6f4-4c6d-916e-ad0839f00185.png”>
代码如下:
// 计算范畴,由哪个开始,哪个完结
const calculateRange = () => {
// 获取内部和外部容器
// 内部容器
const container = getTargetElement(containerTarget);
// 外部容器
const wrapper = getTargetElement(wrapperTarget);
if (container && wrapper) {
const {
// 滚动间隔顶部的间隔。设置或获取位于对象最顶端和窗口中可见内容的最顶端之间的间隔
scrollTop,
// 内容可视区域的高度
clientHeight,
} = container;
// 依据内部容器的 scrollTop 算出曾经“滚过”多少项
const offset = getOffset(scrollTop);
// 可视区域的 DOM 个数
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);
// 设置外部容器的高度,总的高度 - 上方高度
// @ts-ignore
wrapper.style.height = totalHeight - offsetTop + 'px';
// margin top 为上方高度
// @ts-ignore
wrapper.style.marginTop = offsetTop + 'px';
// 设置最初显示的 List
setTargetList(list.slice(start, end).map((ele, index) => ({
data: ele,
index: index + start,
})),
);
}
};
其它就是这个函数的辅助函数了,包含:
- 依据内部容器以及外部每一项的高度,计算出可视区域内的数量:
// 依据内部容器以及外部每一项的高度,计算出可视区域内的数量
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
// 晓得每一行的高度 - number 类型,则依据容器计算
if (isNumber(itemHeightRef.current)) {return Math.ceil(containerHeight / itemHeightRef.current);
}
// 动静指定每个元素的高度状况
let sum = 0;
let endIndex = 0;
for (let i = fromIndex; i < list.length; i++) {
// 计算每一个 Item 的高度
const height = itemHeightRef.current(i, list[i]);
sum += height;
endIndex = i;
// 大于容器宽度的时候,进行
if (sum >= containerHeight) {break;}
}
// 最初一个的下标减去开始一个的下标
return endIndex - fromIndex;
};
- 依据 scrollTop 计算下面有多少个 DOM 节点:
// 依据 scrollTop 计算下面有多少个 DOM 节点
const getOffset = (scrollTop: number) => {
// 每一项固定高度
if (isNumber(itemHeightRef.current)) {return Math.floor(scrollTop / itemHeightRef.current) + 1;
}
// 动静指定每个元素的高度状况
let sum = 0;
let offset = 0;
// 从 0 开始
for (let i = 0; i < list.length; i++) {const height = itemHeightRef.current(i, list[i]);
sum += height;
if (sum >= scrollTop) {
offset = i;
break;
}
}
// 满足要求的最初一个 + 1
return offset + 1;
};
- 获取上部高度:
// 获取上部高度
const getDistanceTop = (index: number) => {
// 每一项高度雷同
if (isNumber(itemHeightRef.current)) {
const height = index * itemHeightRef.current;
return height;
}
// 动静指定每个元素的高度状况,则 itemHeightRef.current 为函数
const height = list
.slice(0, index)
// reduce 计算总和
// @ts-ignore
.reduce((sum, _, i) => sum + itemHeightRef.current(i, list[index]), 0);
return height;
};
- 计算总的高度:
// 计算总的高度
const totalHeight = useMemo(() => {
// 每一项高度雷同
if (isNumber(itemHeightRef.current)) {return list.length * itemHeightRef.current;}
// 动静指定每个元素的高度状况
// @ts-ignore
return list.reduce((sum, _, index) => sum + itemHeightRef.current(index, list[index]),
0,
);
}, [list]);
最初裸露一个滚动到指定的 index 的函数,其次要是计算出该 index 间隔顶部的高度 scrollTop,设置给内部容器。并触发 calculateRange 函数。
// 滚动到指定的 index
const scrollTo = (index: number) => {const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
// 滚动
container.scrollTop = getDistanceTop(index);
calculateRange();}
};
思考与总结
对于高度绝对比拟确定的状况,咱们做虚构滚动还是绝对简略的,但如果高度不确定呢?
或者换另外一个角度,当咱们的滚动不是纵向的时候,而是横向,该如何解决呢?
本文已收录到集体博客中,欢送关注~