Q27 · React / Vue 优化

虚拟列表是如何实现的?什么场景下使用?

虚拟列表windowing长列表有 Demo

⚡ 速记答案(30 秒)

  • 原理:只渲染可视区域内的元素,通过计算滚动位置动态替换内容
  • 核心:固定容器高度 + 绝对定位 + 计算可见元素范围 + 动态渲染
  • 场景:长列表(>100 项)、无限滚动、数据表格
  • :react-window、react-virtualized、vue-virtual-scroller

📖 详细讲解

虚拟列表原理


核心思想


+-------------------+
|    占位区域         | ← 上方不可见区域(padding-top)
+-------------------+
|   可见项 1         |
|   可见项 2         | ← 实际渲染(10-20 个)
|   可见项 3         |
+-------------------+
|    占位区域         | ← 下方不可见区域(padding-bottom)
+-------------------+

实现步骤


1. 容器设置固定高度和 overflow: auto

2. 监听滚动事件

3. 计算当前可见范围

4. 只渲染可见范围内的元素

5. 使用 padding 或 transform 撑开滚动区域


性能对比


方案1000 项渲染时间内存占用
普通列表~200ms
虚拟列表~10ms

使用场景


• 消息列表(聊天)

• 数据表格

• 订单列表

• 日志展示

💻 代码示例

简易虚拟列表实现
import { useState, useRef, useEffect, useMemo } from 'react';

interface VirtualListProps<T> {
  items: T[];
  itemHeight: number;
  containerHeight: number;
  renderItem: (item: T, index: number) => React.ReactNode;
}

function VirtualList<T>({ items, itemHeight, containerHeight, renderItem }: VirtualListProps<T>) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  const { startIndex, endIndex, visibleItems, paddingTop, paddingBottom } = useMemo(() => {
    const totalHeight = items.length * itemHeight;
    const startIndex = Math.floor(scrollTop / itemHeight);
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    const endIndex = Math.min(startIndex + visibleCount + 1, items.length);
    
    return {
      startIndex,
      endIndex,
      visibleItems: items.slice(startIndex, endIndex),
      paddingTop: startIndex * itemHeight,
      paddingBottom: (items.length - endIndex) * itemHeight,
    };
  }, [items, itemHeight, containerHeight, scrollTop]);

  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop);
  };

  return (
    <div
      ref={containerRef}
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ paddingTop, paddingBottom }}>
        {visibleItems.map((item, i) => (
          <div key={startIndex + i} style={{ height: itemHeight }}>
            {renderItem(item, startIndex + i)}
          </div>
        ))}
      </div>
    </div>
  );
}
💡
面试技巧:回答性能问题时,先说指标和标准,再讲优化手段,最后结合实际项目经验。