This article is the eighteenth of a series of articles explaining the source code of ahoos in simple language, which has been organized into document- address . I think it's not bad, give a star to support it, thanks.
Introduction
Hooks that provide virtualized list capabilities are used to solve the problems of slow rendering of the first screen and scrolling freezes when rendering massive data.
Details can be found on the official website , and the source code of the article can be found here .
Implementation principle
Its implementation principle monitors the scroll event of the outer container and when its size changes, triggering the calculation logic to calculate the height and marginTop value of the inner container.
Implementation
Its monitoring scroll logic is as follows:
// 当外部容器的 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,
},
);
Among them, calculateRange is very important. It basically implements the main process logic of virtual scrolling. It mainly does the following things:
- Gets the totalHeight of the entire inner container.
- Calculate how many items have been "scrolled" according to the scrollTop of the outer container, the value is offset.
- According to the height of the outer container and the current start index, get the visibleCount that the outer container can carry.
- And calculate the start index (start) and (end) according to the overscan (the number of DOM nodes displayed above and below the viewport).
- Get the distance to the beginning of its distance (offsetTop) according to the start index.
- Finally, set the height and marginTop of the inner container according to offsetTop and totalHeight.
There are many variables, which can be clearly understood by combining the following figure:
code show as below:
// 计算范围,由哪个开始,哪个结束
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,
})),
);
}
};
Others are the helper functions of this function, including:
- Calculate the amount in the viewable area based on the height of the outer container and each item inside:
// 根据外部容器以及内部每一项的高度,计算出可视区域内的数量
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;
};
- Calculate how many DOM nodes there are based on scrollTop:
// 根据 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;
};
- Get the upper height:
// 获取上部高度
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;
};
- Calculate the total 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]);
Finally, a function that scrolls to the specified index is exposed, which mainly calculates the height scrollTop from the top of the index and sets it to the outer container. And trigger the calculateRange function.
// 滚动到指定的 index
const scrollTo = (index: number) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
// 滚动
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
Thinking and Summarizing
For the case where the height is relatively certain, it is relatively simple for us to do virtual scrolling, but what if the height is uncertain?
Or another angle, when our scrolling is not vertical, but horizontal, how to deal with it?
This article has been included in the personal blog , welcome to pay attention~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。