本文是深入浅出 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 非常重要,它基本实现了虚拟滚动的主流程逻辑,其主要做了以下的事情:
变量很多,可以结合下图,会比较清晰理解:
代码如下:
// 计算范围,由哪个开始,哪个结束
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 节点
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();
}
};
对于高度相对比较确定的情况,我们做虚拟滚动还是相对简单的,但假如高度不确定呢?
或者换另外一个角度,当我们的滚动不是纵向的时候,而是横向,该如何处理呢?
本文已收录到个人博客中,欢迎关注~
是否有类似“RVMuse1”或“RVMuselist[0]”之类的内容而不是键入整个版本号。在任何时候,我们都会看到一个可能包含5个或更多ruby的列表,我们可以轻松地键入一个数字而不是X.X.X。这也有助于rvmgemset。 最佳答案 这在RVM2.0中是可能的=>https://docs.google.com/document/d/1xW9GeEpLOWPcddDg_hOPvK4oeLxJmU3Q5FiCNT7nTAc/edit?usp=sharing-知道链接的任何人都可以发表评论
一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame
在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
我正在使用Rails3.2.3和Ruby1.9.3p0。我发现我经常需要确定某个字符串是否出现在选项列表中。看来我可以使用Ruby数组.includemethod:或正则表达式equals-tildematchshorthand用竖线分隔选项:就性能而言,一个比另一个好吗?还有更好的方法吗? 最佳答案 总结:Array#include?包含String元素,在接受和拒绝输入时均胜出,对于您的示例只有三个可接受的值。对于要检查的更大的集合,看起来Set#include?和String元素可能会获胜。如何测试我们应该根据经验对此进行测试
这是我发现自己偶尔想做的事情。假设我有一个参数列表。在Lisp中,我可以像这样`(imaginary-function,@args)为了调用将数组从一个元素转换为正确数量的参数的函数。Ruby中是否有类似的功能?或者我只是在这里使用了一个完全错误的成语? 最佳答案 是的!它被称为splat运算符。a=[1,44]p(*a) 关于Ruby:如何将数组拼接成Lisp风格的列表?,我们在StackOverflow上找到一个类似的问题: https://stackov
@locations=Location.all#currentlistingall@locations=Location.slice(5)orLocation.split(5)使用Ruby,我试图将我的列表分成4列,每列限制为5个;然而,切片或拆分似乎都不起作用。知道我可能做错了什么吗?任何帮助是极大的赞赏。 最佳答案 您可能想使用in_groups_of:http://railscasts.com/episodes/28-in-groups-of这是RyanBates在railscast中的示例用法:
ruby中有没有一个很好的方法来删除可枚举列表中的重复项(即拒绝等) 最佳答案 对于数组你可以使用uniq()方法a=["a","a","b","b","c"]a.uniq#=>["a","b","c"]所以如果你只是(1..10).to_a.uniq或%w{antbatcatant}.to_a.uniq因为无论如何,几乎所有您实现的方法都将作为Array类返回。 关于Ruby删除可枚举列表中的重复项,我们在StackOverflow上找到一个类似的问题: h
我使用脚手架和Rails3创建了2个模型。模型是位置和作业,每个作业都有一个位置。我在脚手架生成代码中创建了所需的引用调用,但是当我查看创建新作业的View时,我看到的只是一个文本框,我应该在其中添加location_id。我怎样才能让它变成下拉菜单以获得更好的用户体验? 最佳答案 想象一下,您有每个位置的titleAPI:http://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/collection_select 关于r
1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>