构建你自己的虚拟滚动 - 第一部分 第一部分 什么是窗口?让我们来做一些简单的数学运算 示例代码性能和动态高度

2025-05-24

构建您自己的虚拟卷轴 - 第一部分

第一部分

什么是窗口?

让我们做一些简单的数学运算

示例代码

性能与动态高度

这是一个由两部分组成的系列:

第一部分

构建自己的虚拟滚动(窗口)其实并不像听起来那么难。我们将先构建一个简单的虚拟滚动,其中每行的高度都是固定的,然后再讨论当高度动态变化时该如何处理。

在深入探讨技术细节之前,让我们先了解一下虚拟滚动背后的基本概念

重要提示:
本文并非“你应该自己构建虚拟卷轴”的文章,而只是一篇解释如何构建虚拟卷轴的文章。我认为,即使你不亲自实现,了解其工作原理也会非常有益。

什么是窗口?

在常规滚动中,我们有一个可滚动的容器(或视口)和内容,比如说 - 项目列表。

可滚动容器的高度小于内部内容,因此浏览器会显示滚动条,并且仅显示部分内容,具体取决于滚动条的位置。

你可以把视口想象成一个窗口,内容位于窗口后面。用户只能看到窗口后面的部分:

滚动容器就像上下移动内容:

虚拟滚动

在虚拟滚动中,我们不会在屏幕上显示整个内容,以减少 DOM 节点渲染和计算量。

我们“欺骗”用户,让他们认为整个内容都已渲染完毕,因为我们总是只渲染窗口内的部分,并在顶部和底部渲染更多内容以确保平滑过渡。

请注意,我们仍然需要以全高呈现内容(就好像所有列表项都已呈现一样),否则,滚动条的尺寸将不正确,从而在底部和顶部留下空白:

当用户滚动时,我们会重新计算要在屏幕上添加或删除的节点:
正在加载 gif...

你也可以想象一下,就像走在一座桥上,它就在你面前建造,就在你身后被摧毁。从你的角度来看,这感觉就像走在一座完整的桥上,你不会感觉到其中的区别。

让我们做一些简单的数学运算

对于简单的解决方案,我们假设我们知道列表的长度并且每行的高度是固定的。

解决方案是:
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
PREV
每个 Web 开发人员至少应该查看的 10 个网站
NEXT
使用 Let's Encrypt 和 Nginx 自动进行 SSL