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

React 渲染复杂列表的最佳实践

2024-09-114.6k 阅读

使用 key 属性优化列表渲染

在 React 中渲染列表时,key 属性是一个非常重要的概念。当 React 渲染列表时,它需要一种方法来识别每个列表项,以便有效地更新和管理列表。key 就是用来帮助 React 做到这一点的。

假设我们有一个简单的待办事项列表,每个待办事项都有一个唯一的 id。我们可以这样渲染列表:

import React from 'react';

const todoList = [
  { id: 1, text: 'Learn React' },
  { id: 2, text: 'Build a project' },
  { id: 3, text: 'Deploy the app' }
];

function TodoList() {
  return (
    <ul>
      {todoList.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

export default TodoList;

在这个例子中,我们使用 todo.id 作为 key。这样 React 就能准确地跟踪每个列表项,当列表发生变化时(比如添加或删除一个待办事项),React 可以高效地更新 DOM,而不是重新渲染整个列表。

如果不使用 key,React 会发出警告,并且在列表更新时可能会出现性能问题或错误的行为。例如,如果我们删除列表中的第二项,没有 key 的情况下,React 可能会错误地重新排列剩余的项,而不是仅仅删除第二项。

虚拟列表技术

当列表变得非常大时,一次性渲染所有列表项会导致性能问题。虚拟列表技术可以解决这个问题。虚拟列表只渲染当前可见的列表项,而不是渲染整个列表。

使用第三方库(如 react - virtualized)

react - virtualized 是一个流行的 React 虚拟列表库。它提供了多种组件,如 ListTable 等,用于高效地渲染大型列表。

首先,安装 react - virtualized

npm install react - virtualized

然后,我们可以使用 List 组件来渲染一个大型列表。假设我们有一个包含 10000 个数字的列表:

import React from'react';
import { List } from'react - virtualized';

const rowCount = 10000;

const rowRenderer = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      Item {index + 1}
    </div>
  );
};

function BigList() {
  return (
    <List
      height={400}
      rowCount={rowCount}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
}

export default BigList;

在这个例子中,react - virtualizedList 组件只会渲染当前可见区域内的列表项。heightwidth 属性定义了列表的可视区域大小,rowHeight 定义了每个列表项的高度。rowRenderer 函数用于渲染每个列表项。

自定义虚拟列表实现

虽然使用第三方库很方便,但了解如何自定义实现虚拟列表也很有帮助。自定义实现虚拟列表主要涉及以下几个步骤:

  1. 计算可见项的范围:根据列表容器的高度、列表项的高度以及滚动位置,计算出当前可见的列表项的起始和结束索引。
  2. 渲染可见项:只渲染计算出的可见范围内的列表项。
  3. 处理滚动事件:监听滚动事件,更新可见项的范围。

下面是一个简单的自定义虚拟列表的示例代码:

import React, { useState, useEffect } from'react';

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

function CustomVirtualList() {
  const [scrollTop, setScrollTop] = useState(0);
  const visibleItemCount = Math.floor(window.innerHeight / itemHeight);
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = startIndex + visibleItemCount;

  useEffect(() => {
    const handleScroll = () => {
      setScrollTop(window.pageYOffset);
    };
    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return (
    <div style={{ height: '100vh', overflowY: 'auto' }}>
      <div style={{ height: listData.length * itemHeight }}>
        {listData.slice(startIndex, endIndex).map((item, index) => (
          <div
            key={item}
            style={{
              height: itemHeight,
              lineHeight: `${itemHeight}px`,
              borderBottom: '1px solid #ccc'
            }}
          >
            Item {item}
          </div>
        ))}
      </div>
    </div>
  );
}

export default CustomVirtualList;

在这个代码中,我们首先定义了 listData 作为列表的数据来源,itemHeight 为每个列表项的高度。通过 useState 来跟踪 scrollTop 的值,即当前滚动的位置。visibleItemCount 计算出在当前窗口高度内可以显示的列表项数量。startIndexendIndex 确定了当前可见的列表项的范围。

useEffect 中,我们监听窗口的滚动事件,更新 scrollTop 的值。最后,我们只渲染 startIndexendIndex 之间的列表项。

列表的动态更新

在实际应用中,列表往往需要动态更新,比如添加、删除或修改列表项。

添加列表项

假设我们有一个输入框和一个按钮,用户可以在输入框中输入内容,点击按钮后将输入的内容添加到列表中。

import React, { useState } from'react';

function AddToList() {
  const [list, setList] = useState([]);
  const [inputValue, setInputValue] = useState('');

  const handleInputChange = (e) => {
    setInputValue(e.target.value);
  };

  const handleAddItem = () => {
    if (inputValue) {
      setList([...list, inputValue]);
      setInputValue('');
    }
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        placeholder="Enter item"
      />
      <button onClick={handleAddItem}>Add Item</button>
      <ul>
        {list.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default AddToList;

在这个例子中,我们使用 useState 来管理列表 list 和输入框的值 inputValue。当用户在输入框中输入内容时,handleInputChange 函数更新 inputValue。当用户点击按钮时,handleAddItem 函数将 inputValue 添加到 list 中,并清空输入框。

删除列表项

删除列表项通常需要通过一个唯一的标识来确定要删除的项。假设我们的列表项有一个 id 属性,我们可以这样实现删除功能:

import React, { useState } from'react';

const initialList = [
  { id: 1, text: 'Item 1' },
  { id: 2, text: 'Item 2' },
  { id: 3, text: 'Item 3' }
];

function RemoveFromList() {
  const [list, setList] = useState(initialList);

  const handleDelete = (id) => {
    setList(list.filter(item => item.id!== id));
  };

  return (
    <div>
      <ul>
        {list.map(item => (
          <li key={item.id}>
            {item.text}
            <button onClick={() => handleDelete(item.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default RemoveFromList;

这里,handleDelete 函数使用 filter 方法创建一个新的列表,不包含要删除的项,然后通过 setList 更新列表。

修改列表项

修改列表项可以通过先找到要修改的项,然后更新它。假设我们的列表项是可编辑的,用户可以点击一个按钮进入编辑模式,修改后保存。

import React, { useState } from'react';

const initialList = [
  { id: 1, text: 'Item 1' },
  { id: 2, text: 'Item 2' },
  { id: 3, text: 'Item 3' }
];

function EditListItem() {
  const [list, setList] = useState(initialList);
  const [editingId, setEditingId] = useState(null);
  const [editedText, setEditedText] = useState('');

  const handleEdit = (id) => {
    setEditingId(id);
    setEditedText(list.find(item => item.id === id).text);
  };

  const handleSave = (id) => {
    setList(list.map(item => {
      if (item.id === id) {
        return { id, text: editedText };
      }
      return item;
    }));
    setEditingId(null);
  };

  return (
    <div>
      <ul>
        {list.map(item => (
          <li key={item.id}>
            {editingId === item.id? (
              <input
                type="text"
                value={editedText}
                onChange={(e) => setEditedText(e.target.value)}
              />
            ) : (
              item.text
            )}
            {editingId === item.id? (
              <button onClick={() => handleSave(item.id)}>Save</button>
            ) : (
              <button onClick={() => handleEdit(item.id)}>Edit</button>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default EditListItem;

在这个代码中,editingId 用于跟踪当前处于编辑状态的项的 ideditedText 用于存储用户编辑的文本。handleEdit 函数进入编辑模式,handleSave 函数保存修改后的文本。

优化列表渲染性能的其他方面

避免不必要的重新渲染

在 React 中,组件的重新渲染可能会导致性能问题。对于列表组件,我们可以通过 React.memo 来避免不必要的重新渲染。

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

import React from'react';

const ListItem = React.memo(({ item }) => {
  return <li>{item.text}</li>;
});

export default ListItem;

在这个例子中,React.memo 会对 ListItem 组件的 props 进行浅比较。如果 props 没有变化,ListItem 组件不会重新渲染,从而提高性能。

使用 shouldComponentUpdate(类组件)

在类组件中,我们可以通过 shouldComponentUpdate 方法来控制组件是否应该重新渲染。

import React, { Component } from'react';

class ListItem extends Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.item.text!== this.props.item.text;
  }

  render() {
    return <li>{this.props.item.text}</li>;
  }
}

export default ListItem;

在这个 ListItem 类组件中,shouldComponentUpdate 方法只在 item.text 发生变化时返回 true,表示组件需要重新渲染,否则返回 false,避免不必要的重新渲染。

批量更新

在 React 中,当我们多次调用 setState(对于类组件)或 useStateset 函数时,React 会将这些更新批量处理,以减少不必要的重新渲染。但是,在某些情况下,比如在事件处理函数之外调用 setState,React 可能不会批量处理更新。

在 React 18 之前,我们可以使用 unstable_batchedUpdates 来手动批量更新。从 React 18 开始,React 自动在更广泛的场景下批量更新,包括原生事件处理函数。

处理复杂列表结构

在实际项目中,列表结构可能会非常复杂,比如树形结构的列表。

渲染树形列表

假设我们有一个树形结构的数据,每个节点有 idlabelchildren 属性:

import React, { useState } from'react';

const treeData = [
  {
    id: 1,
    label: 'Parent 1',
    children: [
      { id: 11, label: 'Child 11' },
      { id: 12, label: 'Child 12' }
    ]
  },
  {
    id: 2,
    label: 'Parent 2',
    children: [
      { id: 21, label: 'Child 21' },
      { id: 22, label: 'Child 22' }
    ]
  }
];

function TreeNode({ node }) {
  const [isExpanded, setIsExpanded] = useState(false);

  const toggleExpand = () => {
    setIsExpanded(!isExpanded);
  };

  return (
    <div>
      <div onClick={toggleExpand}>
        {isExpanded? '-' : '+'} {node.label}
      </div>
      {isExpanded && node.children && (
        <ul>
          {node.children.map(child => (
            <TreeNode key={child.id} node={child} />
          ))}
        </ul>
      )}
    </div>
  );
}

function TreeList() {
  return (
    <ul>
      {treeData.map(node => (
        <TreeNode key={node.id} node={node} />
      ))}
    </ul>
  );
}

export default TreeList;

在这个例子中,TreeNode 组件递归地渲染树形结构。isExpanded 状态用于控制节点是否展开,用户点击节点的标题可以切换展开状态。

处理列表中的嵌套列表

有时候列表项本身可能又是一个列表。例如,一个学生列表,每个学生有一个课程列表。

import React from'react';

const students = [
  {
    id: 1,
    name: 'Alice',
    courses: ['Math', 'Science']
  },
  {
    id: 2,
    name: 'Bob',
    courses: ['History', 'English']
  }
];

function StudentList() {
  return (
    <ul>
      {students.map(student => (
        <li key={student.id}>
          {student.name}
          <ul>
            {student.courses.map(course => (
              <li key={course}>{course}</li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

export default StudentList;

在这个代码中,外层列表渲染学生,内层列表渲染每个学生的课程。

列表的排序和过滤

列表排序

对列表进行排序是常见的操作。假设我们有一个包含数字的列表,我们可以通过点击按钮对列表进行升序或降序排序。

import React, { useState } from'react';

const initialList = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];

function SortList() {
  const [list, setList] = useState(initialList);
  const [isAscending, setIsAscending] = useState(true);

  const handleSort = () => {
    const sortedList = [...list].sort((a, b) => {
      if (isAscending) {
        return a - b;
      } else {
        return b - a;
      }
    });
    setList(sortedList);
    setIsAscending(!isAscending);
  };

  return (
    <div>
      <button onClick={handleSort}>
        {isAscending? 'Sort Descending' : 'Sort Ascending'}
      </button>
      <ul>
        {list.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default SortList;

在这个例子中,isAscending 状态用于跟踪当前的排序方式。handleSort 函数对列表进行排序,并切换排序方式。

列表过滤

过滤列表可以根据用户输入或其他条件来显示符合条件的列表项。假设我们有一个水果列表,用户可以在输入框中输入水果名称来过滤列表。

import React, { useState } from'react';

const fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

function FilterList() {
  const [filteredFruits, setFilteredFruits] = useState(fruits);
  const [inputValue, setInputValue] = useState('');

  const handleInputChange = (e) => {
    setInputValue(e.target.value);
    const filtered = fruits.filter(fruit =>
      fruit.toLowerCase().includes(inputValue.toLowerCase())
    );
    setFilteredFruits(filtered);
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        placeholder="Filter fruits"
      />
      <ul>
        {filteredFruits.map((fruit, index) => (
          <li key={index}>{fruit}</li>
        ))}
      </ul>
    </div>
  );
}

export default FilterList;

在这个代码中,handleInputChange 函数根据用户输入过滤水果列表,并更新 filteredFruits

性能监控和优化

使用 React DevTools 进行性能分析

React DevTools 是一个强大的工具,可以帮助我们分析组件的渲染性能。在 Chrome 浏览器中,我们可以安装 React DevTools 扩展。

打开 React DevTools 后,切换到“Performance”标签页。我们可以录制组件的渲染过程,查看每个组件的渲染时间、重新渲染次数等信息。通过这些信息,我们可以找出性能瓶颈,比如频繁重新渲染的组件,然后针对性地进行优化。

使用 console.time()console.timeEnd()

在代码中,我们可以使用 console.time()console.timeEnd() 来测量一段代码的执行时间。例如,我们可以测量列表渲染的时间:

import React, { useState } from'react';

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

function PerformanceTest() {
  const [list] = useState(bigList);
  console.time('renderList');
  return (
    <ul>
      {list.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
  console.timeEnd('renderList');
}

export default PerformanceTest;

在这个例子中,console.time('renderList') 开始计时,console.timeEnd('renderList') 结束计时,并在控制台输出渲染列表所花费的时间。通过这种方式,我们可以直观地看到优化前后代码性能的变化。

响应式列表设计

使用 CSS 媒体查询

在设计列表时,我们需要考虑不同屏幕尺寸的适配。CSS 媒体查询是实现响应式设计的常用方法。

假设我们有一个列表,在大屏幕上每行显示三个列表项,在小屏幕上每行显示一个列表项。

/* styles.css */
.list {
  display: flex;
  flex-wrap: wrap;
}

.list-item {
  width: 33.33%;
  padding: 10px;
}

@media (max - width: 600px) {
 .list-item {
    width: 100%;
  }
}
import React from'react';
import './styles.css';

const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'];

function ResponsiveList() {
  return (
    <div className="list">
      {items.map((item, index) => (
        <div className="list-item" key={index}>{item}</div>
      ))}
    </div>
  );
}

export default ResponsiveList;

在这个例子中,通过媒体查询,当屏幕宽度小于 600px 时,列表项的宽度变为 100%,实现了响应式布局。

使用 React 响应式库(如 react - responsive)

react - responsive 是一个专门用于 React 的响应式库。它可以让我们在 React 组件中更方便地进行响应式设计。

首先,安装 react - responsive

npm install react - responsive

然后,我们可以这样使用它:

import React from'react';
import { useMediaQuery } from'react - responsive';

const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'];

function ResponsiveList() {
  const isSmallScreen = useMediaQuery({ maxWidth: 600 });
  const itemWidth = isSmallScreen? '100%' : '33.33%';

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap' }}>
      {items.map((item, index) => (
        <div
          key={index}
          style={{ width: itemWidth, padding: '10px' }}
        >
          {item}
        </div>
      ))}
    </div>
  );
}

export default ResponsiveList;

在这个代码中,useMediaQuery 钩子函数根据屏幕宽度返回一个布尔值,我们根据这个值来设置列表项的宽度,实现响应式布局。

通过以上这些最佳实践,我们可以更高效、更灵活地在 React 中渲染复杂列表,提升应用的性能和用户体验。无论是处理大型列表、动态更新,还是复杂的列表结构、响应式设计等方面,都有相应的技术和方法来解决。在实际项目中,根据具体需求选择合适的方法进行优化是非常重要的。