发布于 2026-01-06 0 阅读
0

我在为 React DEV 的全球展示与分享挑战赛(由 Mux 呈现:Pitch Your Projects!)构建自己的虚拟化列表库的过程中学到了什么!

我从构建自己的 React 虚拟化列表库中学到了什么

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

虚拟化列表与非虚拟化列表

我正在开发的一个项目需要显示一个联系人列表,平均每次显示 5000 个联系人。这意味着需要在屏幕上渲染 5000 个包含大量嵌套组件的复杂 DOM 元素。我们一开始就知道,渲染这么多元素会对 DOM 造成很大的负担。而 React 一如既往地尽职尽责,即使会严重影响渲染性能,只要我们把所有 DOM 元素映射到视图中,它也会毫不犹豫地渲染它们。

{
  LIST_WITH_5000_ITEMS.map(item => <ComplexListItem {...item} />)
}
Enter fullscreen mode Exit fullscreen mode

因此,我们选择了一种更好的方法。最初的方法是使用无限滚动组件,每次加载 250 个联系人,滚动到列表末尾后再加载另外 250 个联系人。这提高了初始渲染性能,但随着滚动到列表末尾,速度会明显变慢。这种方法并没有解决最初的问题。

我们的第二种方法是使用一种叫做虚拟列表的技术。这解决了大部分与列表 DOM 和滚动相关的底层渲染和性能问题,我们终于可以专注于优化联系人列表逻辑中与 DOM 无关的问题,例如繁重的 fetch API 请求。

什么是虚拟化列表?

虚拟列表是一种技术,它将可滚动视图中的大型列表项进行虚拟渲染,仅显示滚动视图窗口中可见的项。这听起来可能有点复杂,但简单来说,虚拟列表只渲染屏幕上可见的项。

为什么需要这样做?

假设有一个应用程序需要处理一个包含大量项目(例如 10000 行)的列表。渲染这个列表的最佳且性能最高的方案是什么?有人会说使用分页列表,在页面切换时卸载相应的行。但是,无限滚动视图又如何呢?这种视图会在我们向下滚动时,在当前视图底部添加新的行。每一行都会被渲染,并且随着我们向下滚动,会不断渲染更多的行。

我们可以设置一个一次只能显示 20 个项目的视图窗口,但这样却要渲染数万个 DOM 元素。这效率极低,会导致滚动卡顿和列表无响应。这可不行。

理想情况下,我们只需要渲染可见的项目。视图窗口之外的项目不需要渲染资源。只有当它们进入可见窗口时才需要渲染。这就是虚拟化列表的用武之地

这是个新想法吗?

由 Brian Vaughn 开发的 react-virtualized

不,实际上,虚拟列表的概念已经存在很长时间了。Android 开发早在 2014 年就有了 RecyclerView React Native 也开箱即用地提供了VirtualizedList组件,而 React 开发者则可以使用名为react-virtualized 的高度可扩展库。虽然这些库和功能在实现方式上可能有所不同,但它们都试图解决同一个问题。

接下来发生了什么?

我对虚拟列表的底层工作原理产生了兴趣。虚拟列表不会像你想象的那样出现滚动条,因为它会动态地渲染新进入视图的项目,并取消渲染移出视图的项目。它的工作方式与非虚拟列表基本相同,侧边栏会显示滚动条,拖动滚动条的功能也相同。

那么,它是如何模仿这些行为的?又是如何追踪视图上的元素的呢?当我开始研究虚拟列表的内部运作机制时,这些问题一直萦绕在我的脑海中。不久前,我读过一篇朋友发来的关于虚拟列表的博客,我只记得其中提到,虚拟列表的渲染方式与通常的列表不同,它使用 `position: absolute` 来定位元素,并追踪列表的滚动位置。

于是我开始着手开发自己的虚拟列表实现,并将其写成一个 React 库,我给它起了个很棒的名字:react-virtualized-listview。由于之前用过 react-virtualized,我深受其 API 的启发。同时,我也想要一个比 react-virtualized 提供的各种功能更简洁的库。别误会,react-virtualized 绝对是 React 最好的库之一,但它提供的功能之多,一开始确实让人有点望而却步。我想要的是一个更易于使用的库,并且希望在开发过程中能够理解虚拟列表的工作原理。

以下是代码示例:

const data = [1, 2, 3, 4, 5];

<List
  source={data}
  rowHeight={40}
  renderItem={({ index, style }) => (
    <div key={index} style={style}>
      Hello {index}
    </div>
  )}
/>
Enter fullscreen mode Exit fullscreen mode

那么它是如何运作的呢?

已渲染列表项的属性

假设我们有一个包含 1000 行的列表。假设每行的高度为 20px,那么列表的总高度就是 20000px。这就是虚拟化列表的工作原理。它会创建一个 DOM 元素,其宽度与父级可见窗口的宽度相同,高度则等于列表项总数乘以每项的高度(此处为 20000px)。这样做的目的是为了使滚动条的位置与非虚拟化列表的行为完全一致。因此,在列表上滚动鼠标滚轮和拖动滚动条都能正常工作。此时,DOM 元素为空。

接下来,我们需要跟踪列表中的滚动位置。这是虚拟化的关键部分。滚动位置决定了我们在列表中的索引位置。该索引位置结合可见窗口的高度,决定了哪些索引位置可见,需要渲染。待渲染的列表项会被赋予一个 `position: absolute` 的样式,以及一个 `top` 值,该值由列表项的索引和行高计算得出。因此,我们只渲染与计算出的索引匹配的列表项。

我们用来模拟非虚拟化列表的另一个技巧是过扫描。我们在可见窗口的上方和下方渲染少量不可见的项目,这样在滚动时,看起来就像还有其他项目存在一样,而不是仅仅在进入可见窗口时才突然出现。

一个 CodeSandbox 示例,展示了虚拟化列表的实际应用。

Codesandbox 上的 React 虚拟列表视图

关于虚拟化列表的更多信息。

文章来源:https://dev.to/nishanbajracharya/what-i-learned-from-building-my-own-virtualized-list-library-for-react-45ik