react-window

发布于:2025-08-10 ⋅ 阅读:(29) ⋅ 点赞:(0)

下面,我们来系统的梳理关于 React 虚拟化列表:react-window 的基本知识点:


一、虚拟化列表核心概念

1.1 什么是虚拟化列表?

虚拟化列表(也称为窗口化)是一种只渲染当前可见区域列表项的技术,而不是一次性渲染整个列表。通过仅渲染用户视口内的元素,可以大幅提升大型列表的性能。

1.2 为什么需要虚拟化列表?

  • 性能提升:减少 DOM 节点数量,提高渲染效率
  • 内存优化:避免加载所有数据到内存中
  • 流畅滚动:确保大型列表的平滑滚动体验
  • 快速响应:提升应用整体响应速度

1.3 虚拟化原理

┌───────────────────────┐
│   可视区域 (Viewport)  │
│ ┌───────────────────┐ │
│ │   可见项 1        │ │
│ │   可见项 2        │ │
│ │   可见项 3        │ │
│ └───────────────────┘ │
│                       │
│  不可见项(不渲染)    │
└───────────────────────┘

二、react-window 核心组件

2.1 固定高度列表 (FixedSizeList)

适用于所有项目高度相同的列表

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>第 {index} 行</div>
);

const MyList = () => (
  <List
    height={600}   // 列表可视高度
    width={300}    // 列表宽度
    itemCount={1000} // 总项目数
    itemSize={50}  // 每个项目高度
  >
    {Row}
  </List>
);

2.2 可变高度列表 (VariableSizeList)

适用于项目高度不同的列表

import { VariableSizeList as List } from 'react-window';

const rowHeights = new Array(1000)
  .fill(true)
  .map(() => 25 + Math.round(Math.random() * 50));

const Row = ({ index, style }) => (
  <div style={style}>第 {index} 行,高度: {rowHeights[index]}px</div>
);

const MyList = () => (
  <List
    height={600}
    width={300}
    itemCount={1000}
    itemSize={index => rowHeights[index]} // 动态高度函数
    estimatedItemSize={50} // 预估高度,用于滚动条计算
  >
    {Row}
  </List>
);

2.3 固定尺寸网格 (FixedSizeGrid)

适用于固定尺寸的网格布局

import { FixedSizeGrid as Grid } from 'react-window';

const Cell = ({ columnIndex, rowIndex, style }) => (
  <div style={style}>
    行 {rowIndex}, 列 {columnIndex}
  </div>
);

const MyGrid = () => (
  <Grid
    height={600}
    width={900}
    columnCount={10}    // 总列数
    rowCount={1000}    // 总行数
    columnWidth={90}   // 列宽
    rowHeight={50}     // 行高
  >
    {Cell}
  </Grid>
);

2.4 可变尺寸网格 (VariableSizeGrid)

适用于动态尺寸的网格布局

import { VariableSizeGrid as Grid } from 'react-window';

const columnWidths = new Array(10).fill(true).map(() => 75 + Math.round(Math.random() * 50));
const rowHeights = new Array(1000).fill(true).map(() => 25 + Math.round(Math.random() * 50));

const Cell = ({ columnIndex, rowIndex, style }) => (
  <div style={style}>
    {rowIndex},{columnIndex}
  </div>
);

const MyGrid = () => (
  <Grid
    height={600}
    width={900}
    columnCount={10}
    rowCount={1000}
    columnWidth={index => columnWidths[index]}
    rowHeight={index => rowHeights[index]}
    estimatedColumnWidth={100}
    estimatedRowHeight={50}
  >
    {Cell}
  </Grid>
);

三、核心特性与高级用法

3.1 滚动控制

import { useRef } from 'react';

function MyList() {
  const listRef = useRef();
  
  const scrollToRow200 = () => {
    listRef.current.scrollToItem(200);
  };
  
  const scrollToCenter = () => {
    listRef.current.scrollToItem(300, "center");
  };
  
  return (
    <>
      <button onClick={scrollToRow200}>滚动到第200项</button>
      <button onClick={scrollToCenter}>滚动到第300项(居中)</button>
      
      <FixedSizeList
        ref={listRef}
        height={600}
        width={300}
        itemCount={1000}
        itemSize={50}
      >
        {Row}
      </FixedSizeList>
    </>
  );
}

3.2 无限滚动加载

import { useState, useCallback } from 'react';

const PAGE_SIZE = 20;

function InfiniteList() {
  const [items, setItems] = useState(Array(100).fill().map((_, i) => `项目 ${i + 1}`));
  const [isLoading, setIsLoading] = useState(false);
  
  const loadMoreItems = useCallback(() => {
    if (isLoading) return;
    
    setIsLoading(true);
    
    // 模拟API请求
    setTimeout(() => {
      const newItems = Array(PAGE_SIZE).fill().map(
        (_, i) => `项目 ${items.length + i + 1}`
      );
      
      setItems(prev => [...prev, ...newItems]);
      setIsLoading(false);
    }, 1000);
  }, [isLoading, items.length]);
  
  const Row = useCallback(({ index, style }) => {
    if (index >= items.length) {
      return (
        <div style={style}>
          <button onClick={loadMoreItems}>加载更多</button>
        </div>
      );
    }
    
    return (
      <div style={style}>
        {items[index]}
      </div>
    );
  }, [items, loadMoreItems]);
  
  return (
    <FixedSizeList
      height={600}
      width={300}
      itemCount={items.length + 1} // 额外一项用于"加载更多"
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

3.3 动态尺寸调整

function DynamicSizeList() {
  const listRef = useRef();
  const [rowHeights, setRowHeights] = useState({});
  
  // 更新项目高度
  const setRowHeight = (index, height) => {
    listRef.current.resetAfterIndex(0);
    setRowHeights(prev => ({
      ...prev,
      [index]: height
    }));
  };
  
  const Row = ({ index, style }) => (
    <DynamicRow 
      index={index} 
      style={style} 
      setHeight={setRowHeight}
    />
  );
  
  const getItemSize = (index) => rowHeights[index] || 50;
  
  return (
    <VariableSizeList
      ref={listRef}
      height={600}
      width={300}
      itemCount={1000}
      itemSize={getItemSize}
      estimatedItemSize={50}
    >
      {Row}
    </VariableSizeList>
  );
}

// 动态高度行组件
function DynamicRow({ index, style, setHeight }) {
  const rowRef = useRef();
  
  useEffect(() => {
    if (rowRef.current) {
      setHeight(index, rowRef.current.clientHeight);
    }
  }, [index, setHeight]);
  
  return (
    <div ref={rowRef} style={style}>
      {/* 动态内容 */}
      项目 {index}
      <div style={{ height: 30 + (index % 10) * 5 }}>动态高度内容</div>
    </div>
  );
}

四、性能优化技巧

4.1 使用 PureComponent 或 React.memo

// 使用 React.memo 避免不必要的行渲染
const Row = React.memo(({ index, style }) => {
  return (
    <div style={style}>
      项目 {index}
    </div>
  );
});

// 使用 shouldComponentUpdate
class RowClass extends React.PureComponent {
  render() {
    const { index, style } = this.props;
    return <div style={style}>项目 {index}</div>;
  }
}

4.2 避免内联函数

// 错误 ❌:每次渲染创建新函数
<FixedSizeList>
  {({ index, style }) => <div style={style}>项目 {index}</div>}
</FixedSizeList>

// 正确 ✅:缓存行组件
const Row = React.useCallback(({ index, style }) => (
  <div style={style}>项目 {index}</div>
), []);

<FixedSizeList>
  {Row}
</FixedSizeList>

4.3 合理使用 estimatedItemSize

对于可变尺寸列表,提供准确的预估尺寸可以优化滚动条精度:

<VariableSizeList
  estimatedItemSize={100} // 预估项目尺寸
  // ...其他属性
/>

五、最佳实践与常见场景

5.1 大型数据列表

import { FixedSizeList } from 'react-window';

const BigList = ({ data }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      <h3>{data[index].name}</h3>
      <p>{data[index].description}</p>
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      width={800}
      itemCount={data.length}
      itemSize={120}
    >
      {Row}
    </FixedSizeList>
  );
};

5.2 表格渲染

import { FixedSizeGrid } from 'react-window';

const DataTable = ({ columns, data }) => {
  const Cell = ({ columnIndex, rowIndex, style }) => (
    <div 
      style={{
        ...style,
        padding: '8px',
        borderBottom: '1px solid #eee',
        display: 'flex',
        alignItems: 'center'
      }}
    >
      {data[rowIndex][columns[columnIndex].key]}
    </div>
  );
  
  return (
    <FixedSizeGrid
      height={600}
      width={1000}
      columnCount={columns.length}
      rowCount={data.length}
      columnWidth={150}
      rowHeight={50}
    >
      {Cell}
    </FixedSizeGrid>
  );
};

5.3 图片画廊

import { VariableSizeGrid } from 'react-window';

const ImageGallery = ({ images }) => {
  // 计算列宽和行高
  const columnWidth = 150;
  const rowHeight = 150;
  
  const columnCount = Math.floor(800 / columnWidth);
  const rowCount = Math.ceil(images.length / columnCount);
  
  const Cell = ({ columnIndex, rowIndex, style }) => {
    const index = rowIndex * columnCount + columnIndex;
    if (index >= images.length) return null;
    
    return (
      <div style={style}>
        <img 
          src={images[index].thumbnail} 
          alt={images[index].title}
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      </div>
    );
  };
  
  return (
    <VariableSizeGrid
      height={600}
      width={800}
      columnCount={columnCount}
      rowCount={rowCount}
      columnWidth={() => columnWidth}
      rowHeight={() => rowHeight}
    >
      {Cell}
    </VariableSizeGrid>
  );
};

六、常见问题与解决方案

6.1 滚动位置重置问题

问题:列表数据更新后滚动位置重置
解决方案:使用 key 属性保持列表稳定

<FixedSizeList
  key={stableListId} // 当stableListId不变时保持滚动位置
  // ...其他属性
/>

6.2 滚动条跳动问题

问题:可变尺寸列表滚动条位置不准确
解决方案

  1. 提供准确的 estimatedItemSize
  2. 使用 resetAfterIndex 方法更新尺寸缓存
const listRef = useRef();

// 当项目尺寸变化时
useEffect(() => {
  listRef.current.resetAfterIndex(0);
}, [sizeData]);

6.3 内存泄漏问题

问题:卸载组件后仍有事件监听
解决方案:使用 ref 清理函数

useEffect(() => {
  const listElement = listRef.current;
  return () => {
    listElement?.removeEventListener('scroll', handleScroll);
  };
}, []);

七、与其他库对比

特性 react-window react-virtualized @tanstack/react-virtual
包大小 ~5kB ~15kB ~3kB
性能 优秀 优秀 优秀
维护性 活跃维护 维护较少 活跃维护
功能丰富度 核心功能 功能全面 功能全面
学习曲线 简单 中等 中等
TypeScript支持 优秀 良好 优秀
文档质量 良好 优秀 优秀

八、案例:社交媒体动态流

import { VariableSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

const FeedList = ({ posts }) => {
  // 测量每篇文章高度并缓存
  const sizeMap = useRef({});
  const listRef = useRef();
  
  const setSize = useCallback((index, size) => {
    sizeMap.current = { ...sizeMap.current, [index]: size };
    listRef.current.resetAfterIndex(index);
  }, []);
  
  const getSize = (index) => sizeMap.current[index] || 100;
  
  const Row = ({ index, style }) => (
    <FeedItem 
      post={posts[index]} 
      style={style} 
      index={index}
      setSize={setSize}
    />
  );
  
  return (
    <AutoSizer>
      {({ height, width }) => (
        <VariableSizeList
          ref={listRef}
          height={height}
          width={width}
          itemCount={posts.length}
          itemSize={getSize}
          estimatedItemSize={300}
        >
          {Row}
        </VariableSizeList>
      )}
    </AutoSizer>
  );
};

const FeedItem = React.memo(({ post, style, index, setSize }) => {
  const rowRef = useRef();
  
  useEffect(() => {
    if (rowRef.current) {
      setSize(index, rowRef.current.clientHeight);
    }
  }, [index, setSize, post]);
  
  return (
    <div ref={rowRef} style={style}>
      <div className="post-header">
        <img src={post.user.avatar} alt={post.user.name} />
        <h3>{post.user.name}</h3>
      </div>
      <p>{post.content}</p>
      {post.image && (
        <img 
          src={post.image} 
          alt="Post" 
          style={{ maxWidth: '100%' }}
        />
      )}
      <div className="post-actions">
        <button>点赞</button>
        <button>评论</button>
        <button>分享</button>
      </div>
    </div>
  );
});

九、总结

9.1 react-window 核心优势

  • 轻量高效:极小的包体积,出色的性能
  • 简单易用:直观的 API 设计
  • 灵活性强:支持固定和可变尺寸
  • 良好兼容:完美支持 React 18 新特性
  • 功能完备:覆盖列表、网格等常见场景

9.2 实践总结

  1. 优先选择固定尺寸:当项目尺寸相同时使用 FixedSizeList
  2. 合理预估尺寸:对可变尺寸列表提供准确预估
  3. 避免内联函数:缓存行渲染函数
  4. 使用尺寸缓存:动态测量项目尺寸并缓存
  5. 配合 AutoSizer:自动适应容器尺寸
  6. 虚拟化一切可滚动内容:表格、网格、画廊等

9.3 性能优化矩阵

场景 推荐组件 优化技巧
等高等宽列表 FixedSizeList 使用 React.memo 优化行组件
等高不等宽列表 FixedSizeGrid 精确计算列宽
动态高度列表 VariableSizeList 使用尺寸测量和缓存
大型表格 FixedSizeGrid 分区域渲染
瀑布流布局 VariableSizeGrid 动态计算行列尺寸

网站公告

今日签到

点亮在社区的每一天
去签到