前端面试题 - 25. 虚拟列表怎么实现?

945 阅读2分钟

虚拟列表是一种优化长列表性能的方式,它只渲染可见视图区域内的数据,从而减少渲染的数量,提高页面性能。

原理

虚拟列表的实现方法如下:

  1. 计算可视区域的高度和位置:首先需要计算出可视区域的高度和位置,可以通过 DOM 元素的 offsetTop 和 offsetHeight 属性来计算。
  2. 计算数据总高度和每个数据项的高度:需要知道数据总共的高度和每个数据项的高度,可以通过 DOM 元素的 offsetHeight 属性来计算。
  3. 计算可见数据的起始索引和结束索引:根据可视区域的高度和位置,以及每个数据项的高度,可以计算出可见数据的起始索引和结束索引。
  4. 渲染可见数据:根据可见数据的起始索引和结束索引,从数据源中取出对应的数据,并渲染到页面上。
  5. 监听滚动事件:当用户滚动列表时,需要重新计算可见数据的起始索引和结束索引,并重新渲染可见数据。

虚拟列表的实现还需要注意以下几点:

  1. 数据项的高度:每个数据项的高度可能不一样,需要根据实际情况进行计算。
  2. 缓存数据:为了提高性能,可以对已渲染过的数据进行缓存,避免重复渲染。
    1. 可以使用两个buffer。缓存当前数据和预加载数据。
    2. 需要考虑滚动方向,更新缓存数据。
  3. 异步加载数据:当数据量比较大时,可以使用异步加载方式,分批加载数据,提高渲染性能。

示例

import React, { useState, useEffect, useRef } from "react";
interface VirtualListProps<T> {
  data: T[];
  itemHeight: number;
  renderItem: (item: T) => React.ReactNode;
  containerHeight: number;
  overscanCount: number;
}

const VirtualList = <T,>({
  data,
  itemHeight,
  renderItem,
  containerHeight,
  overscanCount,
}: VirtualListProps<T>) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);
  const start = Math.floor(scrollTop / itemHeight);
  const end = Math.min(start + Math.ceil(containerHeight / itemHeight), data.length);
  const visibleData = data.slice(start, end);
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const handleScroll = () => {
      setScrollTop(container.scrollTop);
    };
    container.addEventListener("scroll", handleScroll);
    return () => {
      container.removeEventListener("scroll", handleScroll);
    };
  }, []);
  const paddingTop = start * itemHeight;
  const paddingBottom = (data.length - end) * itemHeight;
  const totalHeight = data.length * itemHeight;
  return (
    <div
      ref={containerRef}
      style={{ height: containerHeight, overflowY: "scroll" }}
    >
      <div style={{ height: totalHeight, paddingTop, paddingBottom }}>
        {visibleData.map((item, index) => (
          <div key={index} style={{ height: itemHeight }}>
            {renderItem(item)}
          </div>
        ))}
      </div>
    </div>
  );
};

export const VirtualListDemo = () => {
  const data = Array.from({ length: 10000 }, (_, i) => i);
  const itemHeight = 50;
  const renderItem = (item: any) => <div>{item}</div>;
  const overscanCount = 10;
  const containerHeight = 500;

  return (
    <VirtualList
      data={data}
      itemHeight={itemHeight}
      renderItem={renderItem}
      overscanCount={overscanCount}
      containerHeight={containerHeight}
    />
  );
};

export default VirtualList;