带你深入React.js开发实战,从复杂列表的样式到性能优化

473 阅读27分钟

​ ​微信公众号:小武码码码

大家好!接下来,我想和大家继续分享一下在React.js开发中,关于复杂列表的一些经验和思考。我们将从复杂列表的常见样式、开发方式、高度自适应、性能优化等方面展开详细的讨论。

希望通过这篇文章,能够帮助大家在实际的React.js项目中更好地应对复杂列表的开发和优化。那么,让我们开始吧!(文章有点长呦~)

一、React.js中常见的复杂列表样式和使用场景

在实际的前端开发中,我们经常会遇到各种复杂的列表需求。这些列表往往不只是简单的文本展示,而是包含了丰富的样式和交互。下面我就列举几个在React.js项目中常见的复杂列表样式。

1. 卡片式列表

卡片式列表是最常见的一种样式,每个列表项以卡片的形式展示,包含图片、标题、摘要等内容。卡片式列表多用于新闻资讯、产品展示、用户评论等场景。

示例代码:

function CardList({ data }) {
  return (
    <div className="card-list">
      {data.map(item => (
        <div className="card" key={item.id}>
          <img src={item.image} alt={item.title} />
          <div className="card-content">
            <h3>{item.title}</h3>
            <p>{item.summary}</p>
          </div>
        </div>
      ))}
    </div>
  );
}

2. 宫格式列表

宫格式列表将每个列表项展示为等宽等高的方格,多用于图标导航、图片画廊等场景。

示例代码:

function GridList({ data }) {
  return (
    <div className="grid-list">
      {data.map(item => (
        <div className="grid-item" key={item.id}>
          <img src={item.image} alt={item.title} />
          <span>{item.title}</span>
        </div>
      ))}
    </div>
  );
}

3. 时间轴列表

时间轴列表常用于展示按时间排序的事件,每个列表项包含事件发生的时间和具体内容。

示例代码:

function TimelineList({ data }) {
  return (
    <ul className="timeline-list">
      {data.map(item => (
        <li className="timeline-item" key={item.id}>
          <div className="timeline-time">{item.time}</div>
          <div className="timeline-content">
            <h3>{item.title}</h3>
            <p>{item.content}</p>
          </div>
        </li>
      ))}
    </ul>
  );
}

4. 分组列表

分组列表用于将列表项按照某个维度进行分组,每个组包含一个头部和若干列表项。常见的分组维度有类别、时间等。

示例代码:

function GroupList({ data }) {
  return (
    <div className="group-list">
      {data.map(group => (
        <div className="group-item" key={group.id}>
          <div className="group-header">{group.title}</div>
          <ul className="group-content">
            {group.items.map(item => (
              <li key={item.id}>{item.title}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

5. 树形列表

树形列表用于展示具有层级关系的数据,如文件夹、组织架构等。每个列表项可以展开或收起其子项。

示例代码:

function TreeList({ data }) {
  return (
    <ul className="tree-list">
      {data.map(item => (
        <TreeItem key={item.id} item={item} />
      ))}
    </ul>
  );
}

function TreeItem({ item }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <li className="tree-item">
      <div className="tree-title" onClick={() => setExpanded(!expanded)}>
        {item.title}
      </div>
      {expanded && item.children && (
        <ul className="tree-children">
          {item.children.map(child => (
            <TreeItem key={child.id} item={child} />
          ))}
        </ul>
      )}
    </li>
  );
}

以上就是React.js中几种常见的复杂列表样式和使用场景。在实际开发中,我们还会遇到更多的列表类型,如无限滚动列表、分页列表、多级列表等。

总的来说,列表作为最常用的UI组件之一,其样式和交互的复杂程度往往决定了页面的整体体验。接下来,我们就具体看看在React.js中,如何开发和优化这些复杂列表。

二、React.js中复杂列表的几种开发方式

在React.js中开发列表组件,一般有以下几种方式:

1. 使用map()方法直接遍历

最基本的列表渲染方式就是使用数组的map()方法,在JSX中直接将数组映射为一组列表项。

示例代码:

function SimpleList({ data }) {
  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

这种方式简单直接,适用于列表项结构单一、数据量较小的情况。但当列表项复杂或数据量较大时,会存在以下问题:

  • 列表项的复杂度会导致map()方法内部的JSX难以维护。
  • 列表项的重复逻辑难以抽象和复用。
  • 当列表数据量较大时,一次性渲染所有列表项会影响性能。

2. 封装列表项组件

为了解决列表项臃肿和重复的问题,我们可以将其封装为一个单独的组件。

示例代码:

function ListItem({ item }) {
  return (
    <li>
      <h3>{item.title}</h3>
      <p>{item.content}</p>
    </li>
  );
}

function List({ data }) {
  return (
    <ul>
      {data.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
}

这种方式可以将列表项的结构和逻辑抽离出来,提高了代码的可读性和可维护性。但是对于性能问题,还需要进一步优化。

3. 使用虚拟滚动技术

当列表数据量较大时,一次性渲染所有列表项会占用大量的内存和计算资源,导致页面卡顿。这时我们可以使用虚拟滚动(Virtual Scrolling)技术,只渲染视口内的列表项,从而提高页面性能。

常用的虚拟滚动库有react-window和react-virtualized,它们封装了虚拟滚动的核心逻辑,使用起来非常方便。

以react-window为例,我们可以使用其提供的FixedSizeList组件来渲染列表:

import { FixedSizeList } from 'react-window';

function VirtualList({ data }) {
  const rowRenderer = ({ index, style }) => (
    <div style={style}>
      <ListItem item={data[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      width={300}
      itemSize={120}
      itemCount={data.length}
    >
      {rowRenderer}
    </FixedSizeList>
  );
}

在这个例子中,FixedSizeList组件接收了列表的高度、宽度、每项高度以及列表项数量等参数,同时以render prop的方式接收一个渲染函数rowRenderer,用于渲染每个列表项。

FixedSizeList组件会根据滚动位置,自动计算当前视口中应该渲染哪些列表项,并调用rowRenderer函数进行渲染。这样就可以避免一次性渲染大量列表项,从而提高页面性能。

4. 使用分页或无限滚动

对于数据量非常大的列表,比如搜索结果或社交动态,我们还可以使用分页或无限滚动的方式进行加载和展示。

分页指将数据分为多个页面,每次只加载并渲染一个页面的数据。当用户切换页面时,再去加载新的数据。

而无限滚动则是在用户滚动到列表底部时,自动加载下一页的数据并追加到列表中,从而形成一个无限长的列表。

这两种方式都可以有效减少初始加载时间和内存占用,适用于数据量非常大的场景。

以无限滚动为例,我们可以使用IntersectionObserver API来检测列表底部的可见性,从而触发数据的加载:

function InfiniteList({ data, loadMore }) {
  const ref = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          loadMore();
        }
      },
      {
        root: null,
        rootMargin: '0px',
        threshold: 0.1,
      }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) {
        observer.unobserve(ref.current);
      }
    };
  }, [loadMore]);

  return (
    <ul>
      {data.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
      <div ref={ref} style={{ height: 10 }}></div>
    </ul>
  );
}

在这个例子中,我们在列表底部放置了一个高度为10px的div元素,并用useRef获取其引用。

然后在useEffect中,我们创建了一个IntersectionObserver实例,用于监听这个div元素的可见性变化。当其可见性超过10%时,就会触发loadMore函数,加载下一页数据。

同时注意在组件卸载时,要调用observer.unobserve方法,取消对div元素的监听,以避免内存泄漏。

5. 使用列表缓存

对于某些数据相对稳定、重复渲染率高的列表,如联系人列表或todo列表,我们可以在前端对列表数据进行缓存,避免重复请求数据和重复渲染。

列表缓存的核心思路是:在列表首次加载时,将数据缓存到前端(如localStorage或IndexedDB);之后再次渲染列表时,优先从缓存中读取数据,如果缓存数据不存在或已过期,再去请求新的数据。

下面是一个简单的列表缓存Hook:

function useListCache(cacheKey, fetchData, expireTime = 3600000) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const cachedData = localStorage.getItem(cacheKey);

    if (cachedData) {
      const parsedData = JSON.parse(cachedData);

      if (Date.now() - parsedData.timestamp < expireTime) {
        setData(parsedData.data);
        return;
      }
    }

    setLoading(true);

    fetchData().then(data => {
      setData(data);
      setLoading(false);

      localStorage.setItem(
        cacheKey,
        JSON.stringify({
          timestamp: Date.now(),
          data,
        })
      );
    });
  }, [cacheKey, expireTime, fetchData]);

  return { data, loading };
}

这个Hook接收三个参数:缓存的Key、获取数据的函数以及缓存过期时间(默认1小时)。

它首先会检查LocalStorage中是否存在已缓存的数据,如果存在并且未过期,则直接返回缓存数据;如果不存在或已过期,则调用fetchData函数获取新的数据,并将其更新到state和LocalStorage中。

使用这个Hook,我们可以非常方便地给列表组件添加缓存功能:

function CachedList() {
  const { data, loading } = useListCache(
    'my_list',
    () => axios.get('/api/list').then(res => res.data),
    3600000
  );

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <ul>
      {data.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
}

以上就是React.js中开发复杂列表的几种常见方式,包括直接遍历、封装列表项组件、使用虚拟滚动、分页/无限滚动以及列表缓存等。

在实际开发中,我们需要根据列表的复杂度、数据量、更新频率等因素,选择合适的开发方式。通常,对于结构和逻辑都比较复杂的列表,建议优先考虑封装列表项组件;对于数据量较大的列表,建议使用虚拟滚动或分页;对于更新频率低、重复渲染率高的列表,建议使用列表缓存。

接下来,我们重点讨论复杂列表的高度自适应和性能优化问题。

三、复杂列表的高度自适应和优化

在实际开发中,我们经常会遇到这样的需求:列表中每个item的高度都不相同,但是我们需要列表能够自适应item的高度,同时还要保证列表的渲染性能。

这种需求常见于瀑布流布局、聊天气泡等场景。如果处理不当,很容易出现列表高度计算错误、页面闪烁、滚动卡顿等问题。

下面我就介绍几种常见的列表高度自适应和优化方案。

1. 绝对定位+动态高度

最简单的一种方案是使用绝对定位,将每个列表项的top值动态设置为前面所有项的高度之和。

示例代码:

function DynamicHeightList({ data }) {
  const [heights, setHeights] = useState([]);

  const itemRefs = useRef([]);

  useEffect(() => {
    const newHeights = itemRefs.current.map(ref => ref.offsetHeight);
    setHeights(newHeights);
  }, [data]);

  const getItemTop = index => {
    return heights.slice(0, index).reduce((sum, height) => sum + height, 0);
  };

  return (
    <ul style={{ position: 'relative' }}>
      {data.map((item, index) => (
        <li
          key={item.id}
          ref={ref => (itemRefs.current[index] = ref)}
          style={{
            position: 'absolute',
            top: getItemTop(index),
          }}
        >
          {item.content}
        </li>
      ))}
    </ul>
  );
}

在这个例子中,我们首先通过useRef创建了一个ref数组itemRefs,用于保存每个列表项的DOM引用。

然后在useEffect中,我们通过itemRefs获取到每个列表项的实际高度,并将其保存到state中的heights数组。

最后在渲染列表项时,我们通过getItemTop函数计算每个列表项的top值,即前面所有项的高度之和。

这种方案的优点是实现简单,但缺点也很明显:

  • 需要在列表渲染完成后再次获取每项高度,会导致页面闪烁。
  • 当列表项数量较多时,计算top值的时间复杂度为O(n^2),会影响页面性能。
  • 在列表项高度变化时,需要重新计算所有项的top值,效率较低。

2. Flex布局+自适应宽度

另一种思路是使用Flex布局,通过设置flex-wrap: wrap实现多行排列,再通过flex-basis控制每一项的宽度比例,最终实现高度自适应。

示例代码:

function FlexHeightList({ data, columnWidth = 200 }) {
  return (
    <ul
      style={{
        display: 'flex',
        flexWrap: 'wrap',
        margin: -8,
      }}
    >
      {data.map(item => (
        <li
          key={item.id}
          style={{
            flexBasis: columnWidth,
            margin: 8,
          }}
        >
          <div>{item.content}</div>
        </li>
      ))}
    </ul>
  );
}

在这个例子中,我们将列表容器设置为display: flexflex-wrap: wrap,这样列表项就可以多行排列。

然后给每个列表项设置了固定的flex-basis宽度(默认为200px),再通过margin将各项之间留出一定间距。

这样一来,列表项就可以根据自身内容自动调整高度,并且能够自动换行。而我们只需要控制列数(即columnWidth)即可。

这种方案适用于列表项宽度相对固定、高度变化较小的场景,如网格布局、图片画廊等。

3. Grid布局+自适应行高

与Flex类似,我们还可以使用Grid布局来实现列表的高度自适应。

示例代码:

function GridHeightList({ data, columnWidth = 200, rowGap = 16 }) {
  return (
    <ul
      style={{
        display: 'grid',
        gridTemplateColumns: `repeat(auto-fill, ${columnWidth}px)`,
        gridAutoRows: `minmax(auto, max-content)`,
        gridGap: rowGap,
      }}
    >
      {data.map(item => (
        <li key={item.id}>
          <div>{item.content}</div>
        </li>
      ))}
    </ul>
  );
}

在这个例子中,我们将列表容器设置为display: grid,然后通过gridTemplateColumns设置每列的宽度(使用repeat()函数实现自动填充),通过gridAutoRows设置每行的高度(使用minmax()函数实现自适应)。

这样一来,列表项就可以根据自身内容自动调整高度,并且能够在宽度不足时自动换行。而我们只需要控制列宽和行高即可。

这种方案适用于列宽相对固定,但列高不固定的场景,如瀑布流布局等。而且相比Flex,Grid布局在控制行高方面更加灵活和强大。

4. 虚拟列表+动态高度

前面几种方案都是针对数据量较小的列表,一次性渲染所有列表项。但是当列表数据量较大时,一次性渲染会非常卡顿,此时就需要使用虚拟列表了。

虚拟列表的核心思想是:只渲染视口内的列表项,其余列表项"虚拟"存在,等滚动到相应位置时再实际渲染。

但是在允许列表项高度不固定时,虚拟列表的实现就会变得非常复杂:我们需要动态计算每个列表项的位置和高度,还要处理滚动时的位置映射,同时还要保证列表项的复用和回收。

下面是一个基于react-window的动态高度虚拟列表示例:

import { VariableSizeList } from "react-window";

function VirtualizedList({ data, width, height }) {
  const listRef = useRef(null);

  const rowHeights = useRef([]);

  const getRowHeight = (index) => {
    return rowHeights.current[index] || 50;
  };

  const setRowHeight = (index, size) => {
    listRef.current.resetAfterIndex(0);
    rowHeights.current = [
      ...rowHeights.current.slice(0, index),
      size,
      ...rowHeights.current.slice(index + 1),
    ];
  };

  const Row = ({ index, style }) => {
    const rowRef = useCallback(
      (node) => {
        if (rowRef) {
          setRowHeight(index, node.getBoundingClientRect().height);
        }
      },
      [index]
    );
    return (
      <div style={style}>
        <div ref={rowRef}>{data[index].content}</div>
      </div>
    );
  };

  return (
    <VariableSizeList
      ref={listRef}
      width={width}
      height={height}
      itemCount={data.length}
      itemSize={getRowHeight}
    >
      {Row}
    </VariableSizeList>
  );
}

在这个例子中,我们使用了react-window提供的VariableSizeList组件来实现动态高度虚拟列表。

首先,我们通过useRef创建了一个rowHeights数组,用于缓存每一行的高度。getRowHeight函数则用于获取指定行的高度,如果没有缓存则使用默认值50。

然后,我们定义了一个Row组件,用于渲染每一行。在Row组件内部,我们通过useCallback创建了一个ref回调函数rowRef,用于获取行元素的实际高度,并通过setRowHeight函数将其缓存到rowHeights中。

最后,我们将VariableSizeListitemSize属性设置为getRowHeight函数,这样它就可以动态获取每一行的高度了。同时,我们还将Row组件作为children传递给VariableSizeList,从而渲染出整个列表。

这种方案的优点是可以支持任意高度的列表项,并且能够保证较好的渲染性能。但缺点是实现复杂,需要手动管理行高缓存,而且在行高发生变化时,需要重置列表状态,可能会导致闪烁。

5. 分组+虚拟列表

如果列表项的高度变化比较规律,比如可以分成几种尺寸,那么我们可以考虑将列表项按照尺寸分组,然后在组内使用虚拟列表。这样可以简化虚拟列表的实现,提高渲染性能。

示例代码:

function GroupedVirtualList({ data, width, height, itemHeight }) {
  const groupIndices = useMemo(() => {
    const groups = []; // 分组数组
    let groupIndex = 0;

    const groupHeights = data.reduce((acc, _, index) => {
      const groupHeight = acc[groupIndex] || 0;

      if (groupHeight + itemHeight > 200) {
        groupIndex++;
      }

      acc[groupIndex] = (acc[groupIndex] || 0) + itemHeight;

      groups[index] = groupIndex;

      return acc;
    }, []);

    return { groups, groupHeights };
  }, [data, itemHeight]);

  const { groups, groupHeights } = groupIndices;

  const getGroupHeight = index => groupHeights[index];

  const getRowHeight = index => itemHeight;

  const getRowOffset = index => {
    const groupIndex = groups[index];
    const groupOffset = groupHeights.slice(0, groupIndex).reduce((sum, height) => sum + height, 0);
    const rowOffset = (index - groups.slice(0, groupIndex).length) * itemHeight;
    return groupOffset + rowOffset;
  };

  return (
    <VariableSizeList
      width={width}
      height={height}
      itemCount={data.length}
      itemSize={getRowHeight}
      estimatedItemSize={itemHeight}
      itemOffset={getRowOffset}
    >
      {({ index, style }) => (
        <div style={style}>
          <div>{data[index].content}</div>
        </div>
      )}
    </VariableSizeList>
  );
}

在这个例子中,我们首先通过useMemo计算出了每个列表项所属的组索引groups,以及每个组的高度groupHeights。具体逻辑是:遍历列表数据,根据累计高度判断是否需要开启新的分组,然后将当前项的索引记录到groups数组中,并累加当前组的高度到groupHeights数组中。

接下来,我们定义了三个函数:getGroupHeight用于获取指定组的高度,getRowHeight用于获取指定行的高度(固定为itemHeight),getRowOffset用于获取指定行的偏移量(即在整个列表中的位置)。

最后,我们将VariableSizeListitemSize属性设置为getRowHeight,将estimatedItemSize属性设置为itemHeight,将itemOffset属性设置为getRowOffset,这样它就可以根据分组信息计算出每一行的位置和高度了。同时,我们还简化了children的渲染逻辑,直接使用index索引渲染每一行即可。

这种方案的优点是可以支持大数据量的列表,并且能够保证较好的渲染性能。同时,通过分组可以简化虚拟列表的实现,避免了手动管理行高缓存的问题。但缺点是需要预先知道列表项的高度分布,而且在列表项高度变化时,需要重新计算分组信息,可能会导致闪烁。

以上就是我总结的几种常见的复杂列表高度自适应和优化方案,包括:

  1. 绝对定位+动态高度
  2. Flex布局+自适应宽度
  3. Grid布局+自适应行高
  4. 虚拟列表+动态高度
  5. 分组+虚拟列表

每种方案都有其适用场景和优缺点,我们需要根据实际需求和性能要求进行选择和优化。

接下来,我们再来讨论一下复杂列表的其他性能优化手段。

四、复杂列表的性能优化

除了高度自适应之外,复杂列表的性能优化还需要考虑以下几个方面:

1. 减少重新渲染

在列表数据频繁变化时,如果组件的render函数依赖了过多的数据,就会导致列表频繁重新渲染,从而影响性能。

为了避免这种情况,我们需要尽量减少组件的重新渲染。常见的优化手段有:

  • 使用PureComponentReact.memo包裹组件,避免无效的重新渲染。
  • 使用useMemouseCallback缓存计算结果和回调函数,避免重复计算和创建。
  • 合理拆分组件,将变化频率高的部分单独抽离出来,避免影响其他部分。

示例代码:

const MemorizedRow = memo(({ item }) => {
  return <div>{item.content}</div>;
});

function OptimizedList({ data }) {
  const rowData = useMemo(
    () => data.map((item) => ({ ...item, key: item.id })),
    [data]
  );

  const rowRenderer = useCallback(
    ({ index, style }) => {
      return (
        <div style={style}>
          <MemorizedRow item={rowData[index]} />
        </div>
      );
    },
    [rowData]
  );

  return (
    <FixedSizeList
      width={300}
      height={400}
      itemCount={rowData.length}
      itemSize={50}
    >
      {rowRenderer}
    </FixedSizeList>
  );
}

在这个例子中,我们首先使用React.memo创建了一个MemorizedRow组件,用于渲染每一行。这样可以避免Row组件在item数据没有变化时重新渲染。

然后,我们在OptimizedList组件中使用useMemodata数组进行了转换,为每个元素添加了一个key属性。这样可以避免在data数组变化时重新创建rowData数组。

接着,我们使用useCallback创建了一个rowRenderer函数,用于渲染每一行。这样可以避免在组件重新渲染时重新创建该函数。

最后,我们将rowDatarowRenderer传递给FixedSizeList组件,从而渲染出优化后的列表。

2. 避免不必要的DOM操作

在列表滚动时,如果频繁操作DOM,就会导致页面卡顿和闪烁。为了避免这种情况,我们需要尽量避免不必要的DOM操作。

常见的优化手段有:

  • 使用transform替代top/left等属性进行定位,避免触发页面重排。
  • 对于固定高度的列表,使用position: absolute进行定位,避免滚动时重新计算位置。
  • 对于动态高度的列表,使用position: relative进行定位,并通过offsetTop等属性获取位置,避免频繁读取getBoundingClientRect
  • 使用DocumentFragment批量操作DOM,避免频繁触发页面重绘。

示例代码:

function OptimizedList({ data, height }) {
  const listRef = useRef(null);

  const [startIndex, setStartIndex] = useState(0);

  const visibleCount = Math.ceil(height / 50);

  const endIndex = startIndex + visibleCount;

  const visibleData = data.slice(startIndex, endIndex);

  useEffect(() => {
    const handleScroll = () => {
      const { scrollTop } = listRef.current;
      const newStartIndex = Math.floor(scrollTop / 50);
      setStartIndex(newStartIndex);
    };

    listRef.current.addEventListener('scroll', handleScroll);

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

  return (
    <div ref={listRef} style={{ height, overflow: 'auto' }}>
      <div style={{ height: data.length * 50 }}>
        {visibleData.map((item, index) => (
          <div
            key={item.id}
            style={{
              position: 'absolute',
              top: (startIndex + index) * 50,
              left: 0,
              right: 0,
              height: 50,
            }}
          >
            {item.content}
          </div>
        ))}
      </div>
    </div>
  );
}

在这个例子中,我们首先通过useRef获取到了列表容器的DOM引用listRef

然后,我们根据容器高度height和行高50计算出了可见区域的起始索引startIndex和结束索引endIndex,并根据它们截取出了可见区域的数据visibleData

接着,我们在useEffect中监听了容器的scroll事件,并根据滚动位置scrollTop计算出了新的startIndex,从而触发组件重新渲染。

最后,我们将列表容器的高度设置为data.length * 50,将可见区域的列表项渲染出来,并通过position: absolutetop属性设置它们的位置。这样在滚动时,就只需要更新startIndex,而不需要重新计算每一项的位置了。

需要注意的是,这种方案适用于行高固定的情况。如果行高不固定,则需要使用前面提到的动态高度虚拟列表方案。

3. 使用Intersection Observer

在一些场景下,我们需要监听列表项的可见性,比如图片懒加载、曝光统计等。传统的实现方式是监听scroll事件,然后通过getBoundingClientRect等 API 判断元素是否在可视区域内。

但是这种方式有以下缺点:

  • 需要频繁触发scroll事件,可能会导致性能问题。
  • 需要手动计算元素的位置和大小,逻辑复杂且易出错。
  • 无法检测某些特殊情况,比如元素被其他元素遮挡、页面未激活等。

为了解决这些问题,我们可以使用Intersection Observer API。它可以用来监听元素是否进入或离开可视区域,并在状态变化时触发回调函数。而且它的性能较好,不会频繁触发事件。

示例代码:

function LazyList({ data }) {
  const listRef = useRef(null);

  const [visibleIndices, setVisibleIndices] = useState([]);

  useEffect(() => {
    const options = {
      root: listRef.current,
      rootMargin: '0px',
      threshold: 0.5,
    };

    const observer = new IntersectionObserver(entries => {
      const newIndices = [];

      entries.forEach(entry => {
        if (entry.isIntersecting) {
          newIndices.push(entry.target.dataset.index);
        }
      });

      setVisibleIndices(newIndices);
    }, options);

    const itemNodes = listRef.current.querySelectorAll('[data-index]');

    itemNodes.forEach(node => {
      observer.observe(node);
    });

    return () => {
      observer.disconnect();
    };
  }, [data]);

  return (
    <div ref={listRef} style={{ height: 400, overflow: 'auto' }}>
      {data.map((item, index) => (
        <div key={item.id} data-index={index} style={{ height: 50 }}>
          {visibleIndices.includes(index.toString()) ? (
            <img src={item.imageUrl} alt="" />
          ) : (
            <div style={{ height: 50, background: '#eee' }} />
          )}
        </div>
      ))}
    </div>
  );
}

在这个例子中,我们首先通过useRef获取到了列表容器的DOM引用listRef,然后创建了一个状态变量visibleIndices,用于记录当前可见的列表项索引。

接着,我们在useEffect中创建了一个IntersectionObserver实例,并设置了相关参数:

  • root: 指定根元素,这里是列表容器
  • rootMargin: 指定根元素的外边距,用于扩展或缩小视口的判断范围,这里设为0
  • threshold: 指定目标元素的可见程度,取值在0到1之间,这里设为0.5,即目标元素有一半在可视区域内就算可见

然后,我们给观察器设置了一个回调函数,它会在目标元素的可见性发生变化时被调用。在回调函数中,我们通过isIntersecting属性判断目标元素是否可见,如果可见就将其索引添加到newIndices数组中,最后通过setVisibleIndices将其设置为新的可见索引数组。

接下来,我们通过querySelectorAll获取到了所有带有data-index属性的列表项元素,然后通过observe方法将它们添加到观察器中。

最后,我们在组件卸载时通过disconnect方法取消了观察器,避免内存泄漏。

在渲染列表项时,我们通过visibleIndices.includes(index.toString())判断当前项是否可见,如果可见就渲染实际内容(这里是一个图片),否则渲染一个占位元素(这里是一个灰色背景的空div)。

这样一来,就实现了列表项的懒加载功能,只有可见的列表项才会真正渲染,而不可见的列表项只会渲染占位元素,从而提高了页面性能。

以上就是我总结的几种常见的复杂列表性能优化方案,包括:

  1. 减少重新渲染
  2. 避免不必要的DOM操作
  3. 使用Intersection Observer

这些方案可以根据实际场景进行组合和优化,以达到最佳的性能体验。

五、总结

本文主要探讨了以下内容:

  • 如何使用虚拟列表优化长列表渲染性能
  • 如何处理复杂列表的高度自适应问题
  • 如何进一步优化复杂列表的渲染性能

在实际开发中,列表组件是最常见也是最容易引起性能问题的组件之一。为了提升用户体验,我们需要针对不同场景采用不同的优化策略。

对于数据量较小的列表,我们可以采用以下优化方案:

  • 将列表项拆分为单独的组件,并使用React.memoPureComponent优化渲染性能
  • 使用key属性帮助React识别列表项,提高Diff算法效率
  • 使用CSS3硬件加速属性(如transform)提高渲染效率

对于数据量较大的列表,我们可以采用虚拟列表+懒加载的方式进行优化:

  • 使用虚拟列表只渲染可视区域的列表项,避免一次性渲染大量DOM节点
  • 使用IntersectionObserver API监听列表项可见性,实现懒加载功能
  • 对于动态高度的列表,可以使用分组+虚拟列表的方式进行优化

对于复杂列表(如需要自适应宽高、响应式布局等),我们可以采用以下优化方案:

  • 使用position: absolute对列表项进行定位,避免触发页面重排
  • 使用flexgrid布局实现列表项的自适应宽高
  • 使用resize事件监听容器大小变化,动态调整列表布局
  • 必要时可以使用ResizeObserver API监听列表项大小变化

除了上述通用优化方案外,我们还需要针对具体业务场景进行优化,比如:

  • 对于聊天列表,需要监听新消息事件,动态更新列表数据和滚动位置
  • 对于搜索列表,需要监听用户输入事件,动态筛选和排序列表数据
  • 对于分页列表,需要监听用户滚动事件,动态加载下一页数据

总之,列表组件的优化是一个综合性问题,需要从多个角度入手,并且需要不断迭代和优化。只有深入理解业务场景和用户需求,并掌握各种优化技巧和最佳实践,才能找到最佳的优化方案,打造高性能的用户体验。

下面是一些列表组件优化的最佳实践:

  1. 在开发列表组件时,优先考虑使用现成的UI库或组件,如react-windowreact-virtualized等,它们已经对列表渲染做了充分的优化。
  2. 如果要自己实现列表组件,需要将渲染逻辑和数据处理逻辑分离,将复杂的数据处理逻辑放在Web Worker中执行,避免阻塞UI线程。
  3. 在处理大数据量的列表时,要采用分页或无限滚动的方式,避免一次性加载全部数据。同时,要对请求进行节流和缓存,避免重复请求。
  4. 在渲染列表项时,要尽量使用轻量级的组件,避免嵌套过多的组件层级。必要时可以使用shouldComponentUpdateReact.memo对子组件进行性能优化。
  5. 在处理列表项的动态样式时,要使用行内样式或CSS-in-JS方案,避免频繁操作DOM。同时,要使用CSS3硬件加速属性,如transformopacity等,提高渲染效率。
  6. 在处理复杂列表布局时,要合理使用现代CSS布局方案,如flexgrid等。必要时可以使用position: absolute对列表项进行定位,避免触发页面重排。
  7. 在监听列表相关事件时,要注意避免频繁触发事件回调,必要时可以使用节流或防抖技术进行优化。同时,要注意事件的销毁和内存泄漏问题。
  8. 在进行列表性能测试时,要模拟真实的用户场景,如滚动、筛选、排序等,并使用性能工具进行分析和优化,如React DevToolsChrome Performance等。
  9. 在进行列表组件的持续集成和部署时,要进行充分的自动化测试,包括单元测试、集成测试、性能测试等,确保组件的稳定性和可靠性。

总之,列表组件的优化是一个长期而复杂的过程,需要开发者具备扎实的前端基础和丰富的优化经验。同时,也需要与设计师、产品经理等角色密切配合,从用户体验的角度出发,不断优化和迭代。只有这样,才能真正打造出高性能、高质量的列表组件,为用户提供极致的使用体验。

以下是一些补充的列表组件优化技巧:

  1. 在渲染列表项时,对于复杂的列表项内容,可以考虑使用懒加载或延迟渲染的方式,避免一次性渲染过多内容。
  2. 对于需要实时更新的列表(如股票行情、实时排名等),可以考虑使用WebSocket等实时通信技术,避免频繁的Ajax请求。
  3. 在进行列表筛选或排序时,可以考虑使用Web Worker在后台进行计算,避免阻塞UI线程。
  4. 在处理可编辑列表时,可以使用react-window或react-virtualized提供的可编辑列表组件,避免自己实现复杂的编辑逻辑。
  5. 在处理树形结构的列表时,可以使用react-vtree等专门的树形列表组件,避免自己实现复杂的树形结构算法。
  6. 在处理分组列表时,可以使用react-infinite-scroller等无限滚动组件,结合虚拟列表实现高性能的分组列表。
  7. 在列表滚动时,对于一些昂贵的计算或DOM操作,可以使用requestAnimationFrame或requestIdleCallback进行调度,避免页面卡顿。
  8. 在列表项渲染时,对于一些通用的UI组件(如按钮、图标、头像等),可以使用UI组件库提供的现成组件,避免重复实现。
  9. 在进行列表性能优化时,要注意权衡优化成本和收益,避免过度优化导致得不偿失。
  10. 在进行列表组件的重构或迁移时,要遵循渐进增强、平滑迁移的原则,避免一次性大规模重构导致风险。

总之,列表组件的优化是一个系统工程,需要从前端架构、数据流设计、组件设计、性能优化等多个角度进行考虑。同时,也需要与后端开发、测试、运维等团队密切配合,从整个研发流程的角度进行优化。

只有不断地实践、总结、优化,才能设计出真正高质量的列表组件,为用户提供流畅、高效、愉悦的使用体验。让我们一起努力,打造属于自己的高性能列表组件吧!

六、展望

列表组件的优化之路还远未结束,未来还有很多值得探索和优化的方向,如:

  1. 如何利用React Concurrent Mode和Suspense实现更加智能和高效的列表渲染?
  2. 如何利用Web Assembly和WebGL等新技术,突破现有列表组件的性能瓶颈?
  3. 如何利用机器学习和人工智能技术,实现列表组件的自动优化和适配?
  4. 如何利用VR/AR等沉浸式技术,打造全新的列表交互体验?
  5. 如何利用Serverless和Edge Computing等前沿技术,实现列表组件的云端渲染和分发?

这些都是非常有趣和有挑战性的问题,需要我们不断学习和探索。让我们一起展望列表组件的美好未来,为用户打造极致的交互体验,为业务创造更大的价值。

七、参考资料

  1. React Virtualized: GitHub - bvaughn/react-virtualized: React components for efficiently rendering large lists and tabular data
  2. React Window: GitHub - bvaughn/react-window: React components for efficiently rendering large lists and tabular data
  3. React Infinite Scroller: GitHub - danbovey/react-infinite-scroller: ⏬ Infinite scroll component for React in ES6
  4. React VTree: github.com/guigrpa/rea…
  5. Intersection Observer API: Intersection Observer API - Web APIs | MDN
  6. React Concurrent Mode: Introducing Concurrent Mode (Experimental) – React
  7. React Suspense: Suspense for Data Fetching (Experimental) – React
  8. Web Assembly: WebAssembly
  9. WebGL: WebGL Overview - The Khronos Group Inc
  10. React Performance Optimization: Optimizing Performance – React

以上就是我对React列表组件优化的一些总结和思考,希望对大家有所帮助。如果你有任何问题或建议,欢迎随时交流探讨。让我们一起努力,打造属于自己的高性能列表组件,为用户提供极致的使用体验!