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

从零开始创建一个 React 虚拟化/窗口组件 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

从零开始创建一个 React 虚拟化/窗口组件

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

不久前,我所在的开发团队正在开发一款SaaS应用,该应用需要渲染大量数据(该模块的目的是模拟社交媒体)。由于每个项目本身都是相当庞大的React组件,因此我们必须使用渲染优化技术来提升UI性能,而虚拟化是业内最常用的技术之一。
今天,我将详细讲解我当时创建的组件,以便大家更好地理解大多数项目中使用的库的底层原理。这是一个相当高级的演示,因为我不仅讲解了常见的实现方式,还添加了一些我自己的改进。我会引导大家完成所有步骤,让大家彻底理解这个高性能解决方案背后的技巧。轻松渲染海量项目。

我知道你在想什么!既然已经有了久经考验的解决方案,为什么还要重新发明轮子(使用 React Virtualized)呢?为什么还要费劲从头开始创建自己的方案?问题在于,大多数人甚至不了解底层原理,这很危险!不仅会损害你的代码库,也会损害你的知识储备。你不仅可以自定义最终组件的每一个细节,还能了解现有的局限性以及如何改进它们,这将帮助你成为你理想中的优秀开发者。
在我们开始之前,你需要先了解一些基本知识。

  • TypeScript/Javascript(我更喜欢前者)

  • React(当然你也可以选择其他任何 UI 客户端,本演示中使用的是 React)

  • 浏览器工作原理基础


虚拟化

仅就 UI 而言,虚拟化意味着维护/保存一些并非完全存在于渲染画布(在 Web 环境中,即DOM)中的数据。事实上,React 核心架构的最初理念正是基于虚拟 DOM,它只是对虚拟化基本思想的迭代。虚拟化列表的概念并不新鲜,Android/iOS 等原生平台和桌面应用早已开箱即用地实现了这一点。虽然目前还没有浏览器优先的 API 来支持这项技术,但它早已广为人知。当需要渲染的组件列表异常庞大时,与其将所有元素都挂载到 DOM(这会造成大量的资源开销),不如只渲染当前容器视口中预期存在的少量元素。就是这样,这就是关键所在!别开玩笑,就是这么简单。一旦你了解了它的原理,一切就都明白了。

虚拟化


组件结构

让我们定义组件模式,以便明确我们想要实现的目标。



export interface WindowProps {
  rowHeight: number;
  children: Array<JSX.Element>;
  gap?: number;
  isVirtualizationEnabled?: boolean;
}


Enter fullscreen mode Exit fullscreen mode

为了增强功能,我们不再将容器宽度作为 prop 传递。作为一个智能组件,它应该能够自行推断容器宽度(这也是我想要自己构建组件的原因之一)。
此外,作为 React 子组件,我们只接受 JS 元素列表。TypeScript 的限制虽然没有那么具体,但你可以更进一步,只接受具有预定义 prop 结构的特定列表(这又是另一个话题了)。毋庸置疑,所有子组件都需要是结构相似的同质组件。

间隙表示两个元素之间可见的间隙。我们需要预设 rowHeight,因为我们的组件需要固定的行高(虽然我们可以从子元素中提取行高,但这没有必要,因为动态化行高会增加计算开销,这是另一个问题)。isVirtualizationEnabled只是一个额外的属性,用于展示性能优势。


实施细节



 const [containerRef, { height: containerHeight }] = useElementSize<
    HTMLUListElement
  >();
  const [scrollPosition, setScrollPosition] = React.useState(0);


Enter fullscreen mode Exit fullscreen mode

为了实用性,我使用自定义钩子useElementSize
来跟踪Window组件的容器
(您可以自己创建一个,去试试看)
,并使用另一个状态scrollPosition来在滚动时保持容器的顶部滚动高度。



  const onScroll = React.useMemo(
    () =>
      throttle(
        function (e: any) {
          setScrollPosition(e.target.scrollTop);
        },
        50,
        { leading: false }
      ),
    []
  );


Enter fullscreen mode Exit fullscreen mode

这是用于保持容器中滚动位置的回调函数,这里我使用了lodash 的节流功能来进一步优化滚动事件,因为由于浏览器处理 DOM 事件的方式,onScroll 事件会被多次触发(这正是我们使用节流的一个非常好的用例),我每 50 毫秒更新一次滚动位置。

现在我们来谈谈最重要的部分(如何实际渲染子元素)。



// get the children to be renderd
  const visibleChildren = React.useMemo(() => {
    if (!isVirtualizationEnabled)
      return children.map((child, index) =>
        React.cloneElement(child, {
          style: {
            position: "absolute",
            top: index * rowHeight + index * gap,
            height: rowHeight,
            left: 0,
            right: 0,
            lineHeight: `${rowHeight}px`
          }
        })
      );
    const startIndex = Math.max(
      Math.floor(scrollPosition / rowHeight) - bufferedItems,
      0
    );
    const endIndex = Math.min(
      Math.ceil((scrollPosition + containerHeight) / rowHeight - 1) +
        bufferedItems,
      children.length - 1
    );

    return children.slice(startIndex, endIndex + 1).map((child, index) =>
      React.cloneElement(child, {
        style: {
          position: "absolute",
          top: (startIndex + index) * rowHeight + index * gap,
          height: rowHeight,
          left: 0,
          right: 0,
          lineHeight: `${rowHeight}px`
        }
      })
    );
  }, [
    children,
    containerHeight,
    rowHeight,
    scrollPosition,
    gap,
    isVirtualizationEnabled
  ]);


Enter fullscreen mode Exit fullscreen mode

这里我们需要计算要渲染的子元素切片的起始索引和结束索引,并使用指定的属性从 props 中克隆这些子元素。每个子元素都会相对于容器顶部有一个偏移量进行渲染,这个偏移量我们可以通过滚动位置、行高和子元素的索引轻松计算出来。请注意,我们将子元素的位置设置为绝对定位,这是因为容器中普通的 ` display: flex` 属性不起作用。由于 DOM 中 flex 布局的工作方式,它会在初始渲染后触发额外的滚动事件,从而导致无限循环渲染。因此,我们需要使用间隙和偏移量来固定容器中每个子元素的位置。我使用 `useMemo` 只是为了控制渲染周期。
(我使用了React 的`cloneElement`方法,以便将实际元素的渲染与我们的 `Window` 组件解耦。有很多方法可以解决这个问题,例如,您也可以使用`Render-props`模式。)



return (
    <ul
      onScroll={onScroll}
      style={{
        overflowY: "scroll",
        position: "relative"
      }}
      ref={containerRef}
      className="container"
    >
      {visibleChildren}
    </ul>
  );


Enter fullscreen mode Exit fullscreen mode

别忘了把容器的 position 属性设为 relative,否则会一团糟。


绩效指标

为了观察性能提升,我使用了react-fps,它会监控屏幕刷新率,并在组件中添加了一个开关来启用/禁用虚拟化。

虚拟化演示

希望这能帮助您理清其中的细节。如果您有任何改进建议,欢迎留言,我们将尽力使其更加流畅,并适应更多场景。


这是代码的沙箱链接。

https://codesandbox.io/embed/practical-haze-bxfqe9?fontsize=14&hidenavigation=1&theme=dark

还有 GitHub 链接

https://github.com/Akashdeep-Patra/React-virtualization

也欢迎您在其他平台上关注我。

文章来源:https://dev.to/mr_mornin_star/create-a-react-virtualizationwindowing-component-from-scratch-54lj