由浅入深:编写一个 VirtualList 组件

248 阅读4分钟

在前端开发中,当需要展示大量数据时,常常会遇到性能问题。一种常见的解决方案是使用虚拟滚动,其中的核心概念就是 VirtualList 组件。本篇博客将逐步引导您编写一个 VirtualList 组件,让您了解其实现原理和关键思路。

第一步:静态列表

首先,我们从一个静态列表开始。创建一个 React 函数组件,并接收一个 items 数组作为属性:

jsx

复制

import React from 'react';

const VirtualList = ({ items }) => {
  return (
    <div>
      {items.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
};

export default VirtualList;

这段代码会简单地渲染一个包含所有项的列表。但是,当列表项数量庞大时,渲染性能会受到影响。

第二步:滚动容器

为了实现虚拟滚动,我们需要将列表包裹在一个滚动容器中。修改组件代码如下:

jsx

复制

import React from 'react';

const VirtualList = ({ items }) => {
  return (
    <div style={{ overflowY: 'auto', height: '400px' }}>
      {items.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
};

export default VirtualList;

这里,我们给包裹列表的 <div> 添加了 overflowY: 'auto' 样式,以及一个固定的高度(这里设为 400px)。现在,当列表项过多时,滚动容器会出现垂直滚动条。

第三步:可见项计算

为了实现虚拟滚动的核心功能,我们需要根据滚动容器的位置,动态计算出可见的列表项。为此,我们引入 useState 和 useEffect 钩子,并添加一些状态和引用变量:

jsx

复制

import React, { useState, useEffect } from 'react';

const VirtualList = ({ items }) => {
  const [visibleItems, setVisibleItems] = useState([]);

  useEffect(() => {
    // 计算可见项的逻辑
    // ...
  }, [items]);

  return (
    <div style={{ overflowY: 'auto', height: '400px' }}>
      {visibleItems.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
};

export default VirtualList;

我们将可见项的状态命名为 visibleItems,并使用 useEffect 监听 items 数组的变化。在这个 useEffect 钩子中,我们将来实现计算可见项的逻辑。

第四步:可见项计算逻辑

现在,我们来实现计算可见项的逻辑。我们需要获取滚动容器的位置和高度,并根据每个列表项的位置和高度,判断哪些项是可见的。修改 useEffect 钩子中的代码如下:

jsx

复制

import React, { useState, useEffect } from 'react';

const VirtualList = ({ items }) => {
  const [visibleItems, setVisibleItems] = useState([]);

  useEffect(() => {
    const container = document.getElementById('virtual-list-container');
    const containerTop = container.scrollTop;
    const containerBottom = containerTop + container.offsetHeight;

    let startIndex = 0;
    let endIndex = items.length - 1;

    for (let i = 0; i < items.length; i++) {
      const item = document.getElementById(`virtual-list-item-${i}`);
      const itemTop = item.offsetTop;
      const itemBottom = itemTop + item.offsetHeight;

      if (itemBottom >= containerTop && itemTop <= containerBottom) {
        startIndex = i;
        break;
      }
    }

    for (let i = startIndex; i < items.length完成博客的剩余部分:

```jsx
    for (let i = startIndex; i < items.length; i++) {
      const item = document.getElementById(`virtual-list-item-${i}`);
      const itemTop = item.offsetTop;
      const itemBottom = itemTop + item.offsetHeight;

      if (itemTop > containerBottom) {
        endIndex = i - 1;
        break;
      }
    }

    const visible = items.slice(startIndex, endIndex + 1);
    setVisibleItems(visible);
  }, [items]);

  return (
    <div
      id="virtual-list-container"
      style={{ overflowY: 'auto', height: '400px' }}
    >
      {visibleItems.map((item, index) => (
        <div key={index} id={`virtual-list-item-${index}`}>
          {item}
        </div>
      ))}
    </div>
  );
};

export default VirtualList;

在这段代码中,我们首先获取滚动容器的 DOM 元素,并计算出容器的顶部和底部位置。然后,我们遍历每个列表项,并获取它们的 DOM 元素以及位置信息。根据容器的位置和每个列表项的位置,我们确定哪些项是可见的,并将它们存储在 visibleItems 状态中。最后,我们在 JSX 中使用这些可见项来渲染列表。

第五步:性能优化

目前的实现每次滚动都会重新计算可见项,这可能会导致性能问题,尤其是当列表项数量非常大时。为了优化性能,我们可以使用函数节流来限制计算可见项的频率。

首先,我们引入 throttle 函数,它可以延迟函数的执行,并确保在指定时间间隔内最多只执行一次。在组件中引入 throttle 函数:

jsx

复制

import React, { useState, useEffect } from 'react';
import { throttle } from 'lodash';

const VirtualList = ({ items }) => {
  // ...
};

然后,在 useEffect 钩子中使用 throttle 包装计算可见项的逻辑:

jsx

复制

useEffect(() => {
  const updateVisibleItems = () => {
    // 计算可见项的逻辑
    // ...
  };

  const throttledUpdate = throttle(updateVisibleItems, 100);

  const handleScroll = () => {
    throttledUpdate();
  };

  const container = document.getElementById('virtual-list-container');
  container.addEventListener('scroll', handleScroll);

  return () => {
    container.removeEventListener('scroll', handleScroll);
    throttledUpdate.cancel();
  };
}, [items]);

在这段代码中,我们首先定义了一个名为 updateVisibleItems 的函数,它包含了之前计算可见项的逻辑。然后,我们使用 throttle 函数将 updateVisibleItems 函数包装起来,限制其执行频率为每 100 毫秒最多一次。接下来,我们定义了一个 handleScroll 函数,它会在滚动容器发生滚动时被调用,并触发 throttledUpdate 函数。最后,我们在 useEffect 钩子中添加滚动事件监听器,并在组件卸载时取消监听器和清除节流函数。

codeSandBox 源码

总结

通过逐步实现,我们已经完成了一个简单的 VirtualList 组件。它可以根据滚动容器的位置动态计算可见项,并实现了性能优化。当列表项数量庞大时,该组件能够提供流畅的滚动体验,同时节省内存和渲染开销。

当然,这个 VirtualList 组件还有许多改进的空间。您可以考虑添加占位符来保持滚动容器的高度,优化滚动时的渲染性能,或者支持水平滚动等。希望这个由浅入深的编写过程能够帮助您更好地理解 VirtualList 组件的工作原理和实现方式。

感谢阅读本篇博客!如有任何问题或建议