MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 长列表虚拟化技术实现

2021-11-257.2k 阅读

长列表渲染的挑战

在前端开发中,处理长列表数据是一个常见的需求。当列表中的数据量非常大时,直接渲染所有项目会导致严重的性能问题。例如,假设我们有一个包含 10000 条数据的列表,并且每个列表项都有一定的 DOM 结构和样式。如果一次性将这 10000 个列表项渲染到页面上,浏览器需要花费大量的时间来创建和渲染这些 DOM 元素,这会导致页面卡顿,甚至可能使浏览器崩溃。

传统的渲染方式是将所有数据项都挂载到 DOM 树上。随着数据量的增加,DOM 树的规模也会急剧膨胀。DOM 操作是相对昂贵的,浏览器在处理大量 DOM 元素时,无论是渲染还是重排重绘,都需要消耗大量的计算资源。而且,即使这些数据项在屏幕上不可见,它们依然占据着内存和计算资源,这显然是一种浪费。

React 中的长列表渲染问题

在 React 应用中,同样面临着长列表渲染的挑战。React 的虚拟 DOM 机制虽然在一定程度上优化了 DOM 更新的性能,但当处理大规模列表时,依然会遇到性能瓶颈。例如,当列表数据发生变化时,React 需要重新计算虚拟 DOM 的差异,并将这些差异应用到实际的 DOM 上。如果列表数据量巨大,这个计算和更新的过程会变得非常耗时。

假设我们有一个简单的 React 列表组件:

import React from 'react';

const BigList = () => {
  const data = Array.from({ length: 10000 }, (_, i) => i + 1);
  return (
    <ul>
      {data.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
};

export default BigList;

在这个例子中,我们创建了一个包含 10000 个数字的列表。当组件挂载或者数据发生变化时,React 需要处理这 10000 个列表项的渲染和更新,这无疑会对性能产生较大的影响。

虚拟化技术的引入

为了解决长列表渲染的性能问题,虚拟化技术应运而生。虚拟化的核心思想是只渲染当前视口(viewport)内可见的列表项,而不是渲染整个列表。当用户滚动列表时,动态地加载和渲染进入视口的新列表项,并卸载离开视口的列表项。这样,无论列表数据量有多大,始终只有少量的列表项在 DOM 中,大大减少了 DOM 的数量,从而提升了性能。

以一个无限滚动的新闻列表为例,用户在浏览新闻时,通常一次只会关注屏幕上可见的几条新闻。当用户滚动页面时,新的新闻才会加载并显示。虚拟化技术就像是一个窗口,只展示窗口内的内容,而对窗口外的内容进行“隐藏”(实际上是不渲染)。

React 中实现长列表虚拟化的常用库

  1. react - virtualized:这是一个流行的 React 长列表虚拟化库。它提供了一系列组件,如 ListTableGrid 等,用于高效渲染长列表和表格数据。react - virtualized 通过计算视口的位置和大小,动态地渲染可见区域内的列表项。
  2. react - window:这是另一个用于 React 的高性能窗口化渲染库。它的设计目标是提供简单而高效的长列表虚拟化解决方案。react - window 基于 react - virtualized 进行了进一步的优化,在性能和易用性方面都有不错的表现。它提供了 FixedSizeListVariableSizeList 等组件,分别用于固定尺寸和可变尺寸的列表项渲染。
  3. react - virtualized - select:如果你的应用中需要处理长列表的选择框,那么 react - virtualized - select 是一个不错的选择。它基于 react - virtualized 构建,专门用于优化长列表选择框的性能。

使用 react - virtualized 实现长列表虚拟化

  1. 安装:首先,我们需要安装 react - virtualized 库。可以使用 npm 或 yarn 进行安装:
npm install react - virtualized
# 或者
yarn add react - virtualized
  1. 基本使用:以 List 组件为例,假设我们有一个包含用户姓名的长列表:
import React from'react';
import { List } from'react - virtualized';

const users = Array.from({ length: 10000 }, (_, i) => `User ${i + 1}`);

const rowRenderer = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      {users[index]}
    </div>
  );
};

const UserList = () => {
  return (
    <List
      height={400}
      rowCount={users.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
};

export default UserList;

在这个例子中:

  • heightwidth 定义了列表容器的尺寸。
  • rowCount 表示列表项的总数。
  • rowHeight 定义了每个列表项的高度。
  • rowRenderer 是一个函数,用于渲染每个列表项。它接收 index(当前列表项的索引)、key(唯一标识)和 style(用于定位列表项的样式)作为参数。
  1. 自定义列表项样式和行为:我们可以在 rowRenderer 中进一步自定义列表项的样式和行为。例如,为列表项添加点击事件:
import React from'react';
import { List } from'react - virtualized';

const users = Array.from({ length: 10000 }, (_, i) => `User ${i + 1}`);

const rowRenderer = ({ index, key, style }) => {
  const handleClick = () => {
    console.log(`Clicked on ${users[index]}`);
  };
  return (
    <div key={key} style={style} onClick={handleClick}>
      {users[index]}
    </div>
  );
};

const UserList = () => {
  return (
    <List
      height={400}
      rowCount={users.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
};

export default UserList;
  1. 处理动态数据:如果列表数据是动态变化的,例如通过 API 获取新的数据,我们需要更新 rowCountusers 数组。假设我们有一个按钮,点击按钮会添加新用户:
import React, { useState } from'react';
import { List } from'react - virtualized';

const initialUsers = Array.from({ length: 10000 }, (_, i) => `User ${i + 1}`);

const rowRenderer = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      {users[index]}
    </div>
  );
};

const UserList = () => {
  const [users, setUsers] = useState(initialUsers);
  const handleAddUser = () => {
    const newUser = `User ${users.length + 1}`;
    setUsers([...users, newUser]);
  };
  return (
    <div>
      <button onClick={handleAddUser}>Add User</button>
      <List
        height={400}
        rowCount={users.length}
        rowHeight={50}
        rowRenderer={rowRenderer}
        width={300}
      />
    </div>
  );
};

export default UserList;

在这个例子中,我们使用 useState 来管理用户列表数据。当点击“Add User”按钮时,新用户会被添加到列表中,rowCount 也会相应更新,react - virtualized 会重新计算并渲染可见区域内的列表项。

使用 react - window 实现长列表虚拟化

  1. 安装:使用 npm 或 yarn 安装 react - window
npm install react - window
# 或者
yarn add react - window
  1. 使用 FixedSizeListFixedSizeList 适用于所有列表项高度固定的情况。假设我们有一个包含数字的长列表:
import React from'react';
import { FixedSizeList } from'react - window';

const numbers = Array.from({ length: 10000 }, (_, i) => i + 1);

const renderItem = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      {numbers[index]}
    </div>
  );
};

const NumberList = () => {
  return (
    <FixedSizeList
      height={400}
      itemCount={numbers.length}
      itemSize={50}
      width={300}
    >
      {renderItem}
    </FixedSizeList>
  );
};

export default NumberList;

在这个例子中:

  • heightwidth 定义了列表容器的尺寸。
  • itemCount 表示列表项的总数。
  • itemSize 定义了每个列表项的高度。
  • renderItem 是一个函数,用于渲染每个列表项,它接收 indexkeystyle 作为参数。
  1. 使用 VariableSizeList:如果列表项的高度不固定,我们可以使用 VariableSizeList。假设我们有一个包含不同长度文本的列表,每个文本的高度不同:
import React from'react';
import { VariableSizeList } from'react - window';

const texts = [
  'Short text',
  'A bit longer text here',
  'This is a really long text that takes up more space',
  // 更多不同长度的文本
].repeat(3333);

const getItemSize = (index) => {
  // 简单根据文本长度估算高度
  return texts[index].length * 1.5 + 20;
};

const renderItem = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      {texts[index]}
    </div>
  );
};

const TextList = () => {
  return (
    <VariableSizeList
      height={400}
      itemCount={texts.length}
      getItemSize={getItemSize}
      width={300}
    >
      {renderItem}
    </VariableSizeList>
  );
};

export default TextList;

在这个例子中:

  • getItemSize 函数用于根据列表项的索引返回该项的高度。这里我们简单地根据文本长度估算高度。
  • VariableSizeList 会根据 getItemSize 函数返回的值来动态计算和渲染列表项。

虚拟化技术的原理剖析

  1. 视口计算:无论是 react - virtualized 还是 react - window,核心都是计算视口的位置和大小。视口是指当前可见区域,通常由列表容器的尺寸和滚动位置决定。例如,在一个具有固定高度的列表容器中,当用户滚动列表时,滚动条的位置会发生变化,通过滚动条的位置可以计算出视口的起始索引和结束索引。
  2. 列表项渲染:根据视口的计算结果,虚拟化库会只渲染视口内的列表项。例如,react - virtualizedList 组件通过 rowRenderer 函数来渲染每个可见的列表项。react - windowFixedSizeListVariableSizeList 则通过传入的 renderItem 函数来渲染列表项。这些函数会根据列表项的索引和样式来创建相应的 DOM 元素。
  3. 性能优化:由于只渲染视口内的列表项,DOM 的数量被大大减少,这使得浏览器的渲染和重排重绘操作更加高效。同时,虚拟化库还会对列表项的渲染进行优化,例如复用已创建的 DOM 元素,减少创建和销毁 DOM 的开销。

虚拟化技术的高级应用

  1. 分组列表虚拟化:在实际应用中,我们可能会遇到需要对列表进行分组的情况。例如,一个联系人列表,按照字母顺序分组。可以在虚拟化列表的基础上,对分组进行处理。以 react - virtualized 为例,我们可以通过自定义 rowRenderer 函数,在不同组的列表项之间添加分隔符。
import React from'react';
import { List } from'react - virtualized';

const contacts = [
  { group: 'A', name: 'Alice' },
  { group: 'A', name: 'Adam' },
  { group: 'B', name: 'Bob' },
  // 更多联系人
];

const getGroupIndex = (index) => {
  if (index === 0) return 0;
  return contacts[index].group!== contacts[index - 1].group? index : -1;
};

const rowRenderer = ({ index, key, style }) => {
  const groupIndex = getGroupIndex(index);
  if (groupIndex!== -1) {
    return (
      <div key={key} style={style}>
        <h3>{contacts[groupIndex].group}</h3>
        {contacts[index].name}
      </div>
    );
  }
  return (
    <div key={key} style={style}>
      {contacts[index].name}
    </div>
  );
};

const ContactList = () => {
  return (
    <List
      height={400}
      rowCount={contacts.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
};

export default ContactList;

在这个例子中,getGroupIndex 函数用于判断当前列表项是否属于新的分组。如果是新的分组,则在 rowRenderer 中渲染分组标题。

  1. 加载更多和无限滚动:结合虚拟化技术和加载更多或无限滚动功能,可以进一步提升用户体验。以无限滚动为例,当用户滚动到列表底部时,触发加载新数据的操作。在 react - virtualized 中,可以通过监听滚动事件来实现:
import React, { useState, useRef } from'react';
import { List } from'react - virtualized';

const initialData = Array.from({ length: 100 }, (_, i) => i + 1);

const rowRenderer = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      {data[index]}
    </div>
  );
};

const InfiniteList = () => {
  const [data, setData] = useState(initialData);
  const listRef = useRef(null);
  const loadMore = () => {
    const newData = Array.from({ length: 100 }, (_, i) => data.length + i + 1);
    setData([...data, ...newData]);
  };
  const handleScroll = () => {
    const list = listRef.current;
    if (list) {
      const { scrollTop, clientHeight, scrollHeight } = list;
      if (scrollTop + clientHeight >= scrollHeight - 100) {
        loadMore();
      }
    }
  };
  return (
    <List
      height={400}
      rowCount={data.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
      onScroll={handleScroll}
      ref={listRef}
    />
  );
};

export default InfiniteList;

在这个例子中,我们使用 useState 来管理列表数据,useRef 来获取列表组件的引用。通过监听 onScroll 事件,当滚动到距离列表底部 100 像素以内时,触发 loadMore 函数加载新数据。

虚拟化技术的性能优化

  1. 减少重渲染:在 React 中,组件的重渲染可能会影响性能。对于虚拟化列表,我们可以通过 React.memo 或者 shouldComponentUpdate 来优化。例如,对于 react - virtualizedrowRenderer 函数返回的组件,可以使用 React.memo 进行包裹:
import React from'react';
import { List } from'react - virtualized';

const users = Array.from({ length: 10000 }, (_, i) => `User ${i + 1}`);

const UserItem = React.memo(({ index, style }) => {
  return (
    <div style={style}>
      {users[index]}
    </div>
  );
});

const rowRenderer = ({ index, key, style }) => {
  return <UserItem index={index} style={style} key={key} />;
};

const UserList = () => {
  return (
    <List
      height={400}
      rowCount={users.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
};

export default UserList;

这样,只有当 UserItem 的 props 发生变化时,组件才会重新渲染。

  1. 优化样式和布局:尽量减少复杂的 CSS 样式和布局,尤其是在列表项上。复杂的样式和布局会增加浏览器渲染的时间。例如,避免使用 position: absolutefloat 等会导致重排的属性,除非必要。如果可能,使用 display: flexgrid 来实现简单而高效的布局。
  2. 预加载和缓存:对于可能会滚动到的列表项,可以进行预加载。例如,在用户接近列表底部时,提前加载下一批数据,这样可以减少用户等待的时间。同时,可以对已渲染的列表项进行缓存,避免重复渲染相同的内容。

兼容性和注意事项

  1. 浏览器兼容性:虚拟化技术在现代浏览器中通常有较好的支持,但在一些老旧浏览器中可能会出现问题。例如,某些浏览器对 CSS 样式的支持不完全,可能导致列表项的样式显示异常。在开发过程中,需要进行兼容性测试,确保在目标浏览器中都能正常工作。
  2. 无障碍性:在实现虚拟化列表时,要考虑无障碍性。例如,确保屏幕阅读器等辅助技术能够正确读取列表内容。可以通过添加适当的 ARIA 标签来实现,如 aria - labelaria - describedby
  3. 事件处理:当处理列表项的事件时,要注意事件的冒泡和委托。由于虚拟化列表只渲染部分列表项,事件处理可能会与传统的全量渲染列表有所不同。例如,在 react - virtualizedreact - window 中,点击事件可能需要在 rowRendererrenderItem 函数中进行处理,并且要确保事件的传递和处理符合预期。

通过以上对 React 长列表虚拟化技术的详细介绍,从原理到实践,从基本使用到高级应用和性能优化,希望能够帮助开发者在处理长列表数据时,实现高效且流畅的用户体验。无论是选择 react - virtualized 还是 react - window,都可以根据项目的具体需求和特点来灵活应用,解决长列表渲染带来的性能挑战。