构建您自己的虚拟卷轴 - 第一部分
第一部分
什么是窗口?
让我们做一些简单的数学运算
示例代码
性能与动态高度
这是一个由两部分组成的系列:
第一部分
构建自己的虚拟滚动(窗口)其实并不像听起来那么难。我们将先构建一个简单的虚拟滚动,其中每行的高度都是固定的,然后再讨论当高度动态变化时该如何处理。
在深入探讨技术细节之前,让我们先了解一下虚拟滚动背后的基本概念
重要提示:
本文并非“你应该自己构建虚拟卷轴”的文章,而只是一篇解释如何构建虚拟卷轴的文章。我认为,即使你不亲自实现,了解其工作原理也会非常有益。
什么是窗口?
在常规滚动中,我们有一个可滚动的容器(或视口)和内容,比如说 - 项目列表。
可滚动容器的高度小于内部内容,因此浏览器会显示滚动条,并且仅显示部分内容,具体取决于滚动条的位置。
你可以把视口想象成一个窗口,内容位于窗口后面。用户只能看到窗口后面的部分:
虚拟滚动
在虚拟滚动中,我们不会在屏幕上显示整个内容,以减少 DOM 节点渲染和计算量。
我们“欺骗”用户,让他们认为整个内容都已渲染完毕,因为我们总是只渲染窗口内的部分,并在顶部和底部渲染更多内容以确保平滑过渡。
请注意,我们仍然需要以全高呈现内容(就好像所有列表项都已呈现一样),否则,滚动条的尺寸将不正确,从而在底部和顶部留下空白:
你也可以想象一下,就像走在一座桥上,它就在你面前建造,就在你身后被摧毁。从你的角度来看,这感觉就像走在一座完整的桥上,你不会感觉到其中的区别。
让我们做一些简单的数学运算
对于简单的解决方案,我们假设我们知道列表的长度并且每行的高度是固定的。
解决方案是:
1)将整个内容渲染为一个空容器
2)渲染当前可见的节点
3)将它们向下移动到它们应该在的位置。
让我们来分解一下其中的数学原理:
我们的输入是:
- 视口高度
- 项目总数
- 行高(目前固定)
- 当前滚动视口顶部
以下是我们在每个步骤中进行的计算:
渲染全部内容
如前所述,我们需要以完整高度渲染内容,以确保滚动条的高度准确。这等于节点数乘以行高。
渲染当前可见节点
现在我们有了整个容器的高度,我们只需要根据当前滚动位置渲染可见节点。
第一个节点的值等于视口的 scrollTop除以row height。唯一的例外是,我们设置了一些节点的 padding(可配置),以实现平滑滚动:
可见节点的总数由视口的高度除以行高得出,并且我们还添加了填充:
将节点向下移动
当我们在容器内渲染可见节点时,它们会渲染在容器的顶部。我们现在需要做的是将它们向下移动到正确的位置,并留出一个空隙。
要向下移动节点,最好使用transform:translateY来偏移第一个节点,因为它将在 GPU 上运行。这将确保更快的重绘速度和比绝对定位更好的性能。offsetY等于起始节点乘以行高。
示例代码
由于实现可能因框架而异,我使用返回 HTML 字符串的普通函数编写了一个伪实现:
const VirtualScroll = ({ | |
renderItem, | |
itemCount, | |
viewportHeight, | |
rowHeight, | |
nodePadding, | |
}) => { | |
const totalContentHeight = itemCount * rowHeight; | |
let startNode = Math.floor(scrollTop / rowHeight) - nodePadding; | |
startNode = Math.max(0, startNode); | |
let visibleNodesCount = Math.ceil(viewportHeight / rowHeight) + 2 * nodePadding; | |
visibleNodesCount = Math.min(itemCount - startNode, visibleNodesCount); | |
const offsetY = startNode * rowHeight; | |
const visibleChildren = new Array(visibleNodeCount) | |
.fill(null) | |
.map((_, index) => renderItem(index + startNode)); | |
return ` | |
<div ${/* viewport */} | |
style=" | |
height: ${viewportHeight}; | |
overflow: "auto"; | |
" | |
> | |
<div ${/* content */} | |
style=" | |
height: ${totalContentHeight}; | |
overflow: "hidden"; | |
" | |
> | |
<div ${/* offset for visible nodes */} | |
style=" | |
transform: translateY(${offsetY}px); | |
" | |
> | |
${visibleChildren} ${/* actual nodes */} | |
</div> | |
</div> | |
</div> | |
); | |
}; |
下面是使用 React 的一个工作示例:
性能与动态高度
到目前为止,我们已经处理了一个简单的情况,即所有行的高度相同。这使得计算变成了简洁的公式。但是,如果我们有一个函数来计算每行的高度呢?
要回答这个问题并进一步讨论性能问题,您可以查看第二部分,其中我将展示如何使用二进制搜索来实现这一点。
文章来源:https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib