大家都能看得懂的源码之 ahooks useVirtualList 封装虚拟滚动列表
本文是深入浅出 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 值。
变量很多,可以结合下图,会比较清晰理解:

代码如下:
// 计算范围,由哪个开始,哪个结束
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();
}
};
思考与总结
对于高度相对比较确定的情况,我们做虚拟滚动还是相对简单的,但假如高度不确定呢?
或者换另外一个角度,当我们的滚动不是纵向的时候,而是横向,该如何处理呢?
本文已收录到个人博客中,欢迎关注~
大家都能看得懂的源码之 ahooks useVirtualList 封装虚拟滚动列表的更多相关文章
- 大家都能看得懂的源码之ahooks useInfiniteScroll
本文是深入浅出 ahooks 源码系列文章的第十七篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 简介 useInfiniteScroll 封装了常见的无限滚动逻 ...
- 大家都能看得懂的源码 - ahooks useSet 和 useMap
本文是深入浅出 ahooks 源码系列文章的第十篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 今天我们来聊聊 ahooks 中对 Map 和 Set 类型进行状 ...
- 大家都能看得懂的源码(一)ahooks 整体架构篇
本文是深入浅出 ahooks 源码系列文章的第一篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 第一篇主要介绍 ahooks 的背景以及整体架构. React h ...
- 大家都能看得懂的源码 - 如何封装 cookie/localStorage/sessionStorage hook?
本文是深入浅出 ahooks 源码系列文章的第九篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 今天来看看 ahooks 是怎么封装 cookie/localSt ...
- 大家都能看得懂的源码 - ahooks 是怎么处理 DOM 的?
本文是深入浅出 ahooks 源码系列文章的第十三篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 本篇文章探讨一下 ahooks 对 DOM 类 Hooks 使用 ...
- 大家都能看得懂的源码 - 那些关于DOM的常见Hook封装(一)
本文是深入浅出 ahooks 源码系列文章的第十四篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 上一篇我们探讨了 ahooks 对 DOM 类 Hooks 使用 ...
- 大家都能看得懂的源码 - 那些关于DOM的常见Hook封装(二)
本文是深入浅出 ahooks 源码系列文章的第十五篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 本篇接着针对关于 DOM 的各个 Hook 封装进行解读. us ...
- 如何读懂Framework源码?如何从应用深入到Framework?
如何读懂Framework源码? 首先,我也是一个应用层开发者,我想大部分有"如何读懂Framework源码?"这个疑问的,应该大都是应用层开发. 那对于我们来讲,读源码最大的问题 ...
- 玩一把redis源码(一):为redis添加自己的列表类型
2019年第一篇文档,为2019年做个良好的开端,本文档通过step by step的方式向读者展示如何为redis添加一个数据类型,阅读本文档后读者对redis源码的执行逻辑会有比较清晰的认识,并且 ...
随机推荐
- 论文解读(SR-GNN)《Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data》
论文信息 论文标题:Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data论文作者:Qi Zhu, ...
- Maven-打包jar指定main函数所在类的一个例子
问题描述:maven打包jar时,由于带main方法的类没有被加入manifest中,导致执行java -jar mvn-jar-1.0-SNAPSHOT.jar时,会提示没有主清单属性. 解决办法: ...
- Visio Professional之活动图
1 什么叫活动图? 活动图在本质上是一种流程图. 活动图(Activity diagram)是UML用于对系统的动态行为建模的一种常用工具,它描述活动的顺序,表示一个活动到另一个活动的控制流. 2.活 ...
- AI 绘画极简教程
昨天在朋友圈发了几张我用AI绘画工具Disco Diffusion画的画 既然有同学问,就写个极简教程吧,画个图是足够了,想要深入了解还是自行百度吧,可以找到更详细的教程. 第 0 步:学会上网,注册 ...
- Java双重校验单例模式详解
单例模式双重检测java实现: public class Singleton { private volatile static Singleton instance = null; //#1 pub ...
- 啥也不是 -「OI 易犯错误整理」
原帖出自 Nefelibata,不过他不想维护,所以就交给 STrAduts 了 awa.因为一些不可抗力,帖主转移至 XSC062.申请置顶! 前言 Nefelibata:因为笔者弱到无法形容,因此 ...
- AtCoder Beginner Contest 261 F // 树状数组
题目链接:F - Sorting Color Balls (atcoder.jp) 题意: 有n个球,球有颜色和数字.对相邻的两球进行交换时,若颜色不同,需要花费1的代价.求将球排成数字不降的顺序,所 ...
- WebWorker:工作者线程初探
WebWorker:工作者线程初探 参考资料: 1.Web Worker 使用教程 - 阮一峰:http://www.ruanyifeng.com/blog/2018/07/web-worker.ht ...
- 常用的函数式接口_Predicate接口和常用的函数式借楼_Predicate_默认方法and
package com.yang.Test.PredicateStudy; import java.util.function.Predicate; /** * java.util.function. ...
- 字符串的操作和MAth工具类
字符串的操作 常用方法 判断功能方法 equals(String s)判断两个字符串是否相同,区分大小写 equsalsignorecase(String s) 判断两个字符串是否相同,不区分大小写 ...