MST

星途 面试题库

面试题:针对 React 动态大数据列表渲染,如何实现虚拟列表并优化其性能?

假设有一个 React 应用需要展示包含上万条数据的动态列表,传统渲染方式导致性能严重下降。请设计并实现一个虚拟列表方案,确保只有可见区域的列表项被渲染,同时要考虑列表项的动态更新、复用以及与 React 生命周期或 React Hooks 的良好结合。详细描述该方案的设计思路、关键实现步骤以及在不同场景下如何进一步优化性能,例如在列表项高度不一致的情况下如何处理。
29.2万 热度难度
前端开发React

知识考点

AI 面试

面试题答案

一键面试

设计思路

  1. 计算可见区域:通过监听滚动事件,结合容器高度和列表项高度,计算出当前可见区域的起始和结束索引。
  2. 渲染可见项:只渲染可见区域内的列表项,减少 DOM 数量,提升性能。
  3. 列表项复用:利用 React 的 key 机制和状态管理,实现列表项的复用,避免重复渲染相同数据。
  4. 动态更新:使用 React 的状态更新机制,确保数据变化时,能正确更新可见区域内的列表项。

关键实现步骤

  1. 创建容器和样式
    import React, { useState, useRef, useEffect } from'react';
    import './styles.css';
    
    const VirtualList = () => {
      const containerRef = useRef(null);
      return (
        <div className="virtual - list - container" ref={containerRef}>
          {/* 列表项将在这里渲染 */}
        </div>
      );
    };
    
    export default VirtualList;
    
    在 CSS 中设置容器的高度和 overflow 属性,使其可滚动:

.virtual - list - container { height: 400px; overflow - y: scroll; }

2. **计算可见区域**:
```jsx
const VirtualList = () => {
  const containerRef = useRef(null);
  const [startIndex, setStartIndex] = useState(0);
  const [endIndex, setEndIndex] = useState(10);
  const itemHeight = 50;

  useEffect(() => {
    const handleScroll = () => {
      const scrollTop = containerRef.current.scrollTop;
      const newStartIndex = Math.floor(scrollTop / itemHeight);
      const newEndIndex = Math.min(newStartIndex + Math.floor(containerRef.current.clientHeight / itemHeight), data.length);
      setStartIndex(newStartIndex);
      setEndIndex(newEndIndex);
    };
    containerRef.current.addEventListener('scroll', handleScroll);
    return () => {
      containerRef.current.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return (
    <div className="virtual - list - container" ref={containerRef}>
      {/* 列表项将在这里渲染 */}
    </div>
  );
};
  1. 渲染可见项
    const data = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
    const VirtualList = () => {
      const containerRef = useRef(null);
      const [startIndex, setStartIndex] = useState(0);
      const [endIndex, setEndIndex] = useState(10);
      const itemHeight = 50;
    
      useEffect(() => {
        const handleScroll = () => {
          const scrollTop = containerRef.current.scrollTop;
          const newStartIndex = Math.floor(scrollTop / itemHeight);
          const newEndIndex = Math.min(newStartIndex + Math.floor(containerRef.current.clientHeight / itemHeight), data.length);
          setStartIndex(newStartIndex);
          setEndIndex(newEndIndex);
        };
        containerRef.current.addEventListener('scroll', handleScroll);
        return () => {
          containerRef.current.removeEventListener('scroll', handleScroll);
        };
      }, []);
    
      const visibleItems = data.slice(startIndex, endIndex);
      return (
        <div className="virtual - list - container" ref={containerRef}>
          {visibleItems.map((item, index) => (
            <div key={startIndex + index} className="list - item">{item}</div>
          ))}
        </div>
      );
    };
    
    在 CSS 中设置列表项样式:

.list - item { height: 50px; border - bottom: 1px solid #ccc; display: flex; align - items: center; padding: 0 10px; }

4. **列表项复用**:
通过设置合适的 key,React 可以复用列表项。这里使用 `startIndex + index` 作为 key,确保每个可见项有唯一标识。如果列表项内有更复杂的状态管理,可以使用 `useState` 或 `useReducer` 来管理。
5. **动态更新**:
如果数据发生变化,例如新数据添加到 `data` 数组中,可以通过更新 `data` 状态触发重新渲染。由于 `startIndex` 和 `endIndex` 依赖于滚动位置,可见区域会正确更新:
```jsx
const [data, setData] = useState(Array.from({ length: 10000 }, (_, i) => `Item ${i}`));
const addNewItem = () => {
  setData([...data, `New Item ${data.length}`]);
};

然后在组件中添加一个按钮来触发 addNewItem 函数:

return (
  <div>
    <button onClick={addNewItem}>Add New Item</button>
    <div className="virtual - list - container" ref={containerRef}>
      {visibleItems.map((item, index) => (
        <div key={startIndex + index} className="list - item">{item}</div>
      ))}
    </div>
  </div>
);

不同场景下性能优化

  1. 列表项高度不一致

    • 方案一:记录每个项的高度: 创建一个数组记录每个列表项的高度,例如 const itemHeights = Array.from({ length: data.length }, () => Math.floor(Math.random() * 50) + 30);。在计算 startIndexendIndex 时,根据累计高度来计算:
      useEffect(() => {
        const handleScroll = () => {
          let scrollTop = containerRef.current.scrollTop;
          let newStartIndex = 0;
          let totalHeight = 0;
          for (let i = 0; i < itemHeights.length; i++) {
            totalHeight += itemHeights[i];
            if (totalHeight > scrollTop) {
              newStartIndex = i;
              break;
            }
          }
          let newEndIndex = newStartIndex;
          totalHeight = 0;
          while (totalHeight < containerRef.current.clientHeight && newEndIndex < itemHeights.length) {
            totalHeight += itemHeights[newEndIndex];
            newEndIndex++;
          }
          setStartIndex(newStartIndex);
          setEndIndex(newEndIndex);
        };
        containerRef.current.addEventListener('scroll', handleScroll);
        return () => {
          containerRef.current.removeEventListener('scroll', handleScroll);
        };
      }, []);
      
    • 方案二:使用近似高度: 计算平均高度,在计算可见区域时使用平均高度进行近似计算。然后在渲染时,根据实际高度调整布局。例如:
      const averageHeight = itemHeights.reduce((acc, height) => acc + height, 0) / itemHeights.length;
      useEffect(() => {
        const handleScroll = () => {
          const scrollTop = containerRef.current.scrollTop;
          const newStartIndex = Math.floor(scrollTop / averageHeight);
          const newEndIndex = Math.min(newStartIndex + Math.floor(containerRef.current.clientHeight / averageHeight), data.length);
          setStartIndex(newStartIndex);
          setEndIndex(newEndIndex);
        };
        containerRef.current.addEventListener('scroll', handleScroll);
        return () => {
          containerRef.current.removeEventListener('scroll', handleScroll);
        };
      }, []);
      
      渲染时,使用绝对定位和 transform 来调整列表项位置,以适应实际高度。
  2. 大数据量下的性能优化

    • 减少重渲染: 使用 React.memo 包裹列表项组件,防止不必要的重渲染。例如:
      const ListItem = React.memo(({ item }) => {
        return <div className="list - item">{item}</div>;
      });
      
    • 虚拟化库: 可以使用成熟的虚拟化库,如 react - virtualizedreact - window,它们已经对虚拟列表进行了高度优化,支持多种场景,包括列表项高度不一致、固定头部和脚部等。例如使用 react - window
      import { FixedSizeList } from'react - window';
      
      const renderRow = ({ index, key, style }) => {
        return (
          <div key={key} style={style}>
            {data[index]}
          </div>
        );
      };
      
      const VirtualList = () => {
        return (
          <FixedSizeList
            height={400}
            itemCount={data.length}
            itemSize={50}
            width={300}
          >
            {renderRow}
          </FixedSizeList>
        );
      };