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

React 列表排序与过滤的实现

2022-04-115.6k 阅读

React 列表排序与过滤的基础概念

在 React 应用开发中,列表的排序与过滤是极为常见的需求。排序能够帮助用户以特定顺序查看数据,比如按日期先后、字母顺序或者数值大小等。过滤则是让用户从大量数据中筛选出符合特定条件的数据子集,提升数据查找和浏览的效率。

以一个待办事项应用为例,用户可能希望按优先级对任务进行排序,或者过滤出已完成的任务。在电商应用中,用户可能会根据价格对商品列表进行排序,或者过滤出特定品牌的商品。

React 作为前端开发框架,其单向数据流和组件化的特性为实现列表排序与过滤提供了坚实基础。我们将状态存储在父组件中,通过 props 将数据传递给子组件进行展示。当排序或过滤的条件发生变化时,更新父组件状态,进而触发子组件重新渲染,展示符合新条件的列表。

实现列表排序

简单排序逻辑

  1. 准备数据:假设我们有一个包含多个对象的数组,每个对象代表一个学生,包含姓名和成绩属性。
const students = [
    { name: 'Alice', score: 85 },
    { name: 'Bob', score: 78 },
    { name: 'Charlie', score: 92 }
];
  1. 基本排序函数:在 JavaScript 中,数组的 sort 方法可用于对数组元素进行排序。我们可以定义一个比较函数来决定排序规则。例如,按成绩从高到低排序:
function compareByScore(a, b) {
    return b.score - a.score;
}
const sortedStudents = students.sort(compareByScore);
console.log(sortedStudents);

在 React 组件中实现此功能,我们需要将排序逻辑与组件状态和渲染相结合。 3. 在 React 组件中实现排序

import React, { useState } from'react';

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 92 }
    ]);
    const [isSorted, setIsSorted] = useState(false);

    const compareByScore = (a, b) => {
        return b.score - a.score;
    };

    const handleSort = () => {
        const newStudents = [...students].sort(compareByScore);
        setStudents(newStudents);
        setIsSorted(!isSorted);
    };

    return (
        <div>
            <button onClick={handleSort}>
                {isSorted? '恢复顺序' : '按成绩排序'}
            </button>
            <ul>
                {students.map((student, index) => (
                    <li key={index}>
                        {student.name}: {student.score}
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

在上述代码中,useState 钩子用于管理学生列表数据和排序状态。handleSort 函数在按钮点击时,复制当前学生列表,对其进行排序,然后更新学生列表状态。按钮文本根据当前排序状态进行切换。

多字段排序

  1. 需求分析:有时候,我们需要根据多个字段进行排序。比如,先按成绩排序,如果成绩相同,再按名字的字母顺序排序。
  2. 实现多字段排序函数
function compareStudents(a, b) {
    if (a.score!== b.score) {
        return b.score - a.score;
    } else {
        return a.name.localeCompare(b.name);
    }
}
  1. 在 React 组件中应用多字段排序
import React, { useState } from'react';

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 85 }
    ]);
    const [isSorted, setIsSorted] = useState(false);

    const compareStudents = (a, b) => {
        if (a.score!== b.score) {
            return b.score - a.score;
        } else {
            return a.name.localeCompare(b.name);
        }
    };

    const handleSort = () => {
        const newStudents = [...students].sort(compareStudents);
        setStudents(newStudents);
        setIsSorted(!isSorted);
    };

    return (
        <div>
            <button onClick={handleSort}>
                {isSorted? '恢复顺序' : '按成绩和姓名排序'}
            </button>
            <ul>
                {students.map((student, index) => (
                    <li key={index}>
                        {student.name}: {student.score}
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

这里的 compareStudents 函数先比较成绩,如果成绩相同,再通过 localeCompare 方法比较姓名,实现了多字段排序。

实现列表过滤

简单文本过滤

  1. 文本输入过滤逻辑:假设我们有一个学生列表,用户可以通过输入学生名字的部分字符来过滤列表。
import React, { useState } from'react';

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 92 }
    ]);
    const [filterText, setFilterText] = useState('');

    const filteredStudents = students.filter(student =>
        student.name.toLowerCase().includes(filterText.toLowerCase())
    );

    const handleFilterChange = (e) => {
        setFilterText(e.target.value);
    };

    return (
        <div>
            <input
                type="text"
                placeholder="搜索学生"
                value={filterText}
                onChange={handleFilterChange}
            />
            <ul>
                {filteredStudents.map((student, index) => (
                    <li key={index}>
                        {student.name}: {student.score}
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

在上述代码中,useState 钩子用于管理学生列表数据和用户输入的过滤文本。filter 方法遍历学生列表,检查学生名字是否包含用户输入的文本(不区分大小写)。handleFilterChange 函数在输入框内容变化时更新过滤文本状态,从而实时更新过滤后的列表。

复杂条件过滤

  1. 多条件过滤需求:除了文本过滤,我们可能还需要根据其他条件进行过滤。例如,用户可以选择成绩范围来过滤学生列表。
  2. 实现多条件过滤
import React, { useState } from'react';

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 92 }
    ]);
    const [filterText, setFilterText] = useState('');
    const [minScore, setMinScore] = useState(0);
    const [maxScore, setMaxScore] = useState(100);

    const filteredStudents = students.filter(student => {
        const nameMatch = student.name.toLowerCase().includes(filterText.toLowerCase());
        const scoreMatch = student.score >= minScore && student.score <= maxScore;
        return nameMatch && scoreMatch;
    });

    const handleFilterChange = (e) => {
        setFilterText(e.target.value);
    };

    const handleMinScoreChange = (e) => {
        setMinScore(parseInt(e.target.value, 10));
    };

    const handleMaxScoreChange = (e) => {
        setMaxScore(parseInt(e.target.value, 10));
    };

    return (
        <div>
            <input
                type="text"
                placeholder="搜索学生"
                value={filterText}
                onChange={handleFilterChange}
            />
            <input
                type="number"
                placeholder="最小成绩"
                value={minScore}
                onChange={handleMinScoreChange}
            />
            <input
                type="number"
                placeholder="最大成绩"
                value={maxScore}
                onChange={handleMaxScoreChange}
            />
            <ul>
                {filteredStudents.map((student, index) => (
                    <li key={index}>
                        {student.name}: {student.score}
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

这里,我们增加了两个状态 minScoremaxScore 来表示成绩范围。filteredStudents 通过 filter 方法结合名字匹配和成绩范围匹配来过滤学生列表。输入框的 onChange 事件分别更新对应的过滤条件状态,实现复杂条件过滤。

结合排序与过滤

顺序问题分析

当同时实现排序与过滤时,需要考虑执行顺序。一般来说,先进行过滤可以减少排序的数据量,提高性能。但在某些情况下,根据具体需求,也可能先进行排序。

代码实现结合

import React, { useState } from'react';

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 92 }
    ]);
    const [filterText, setFilterText] = useState('');
    const [minScore, setMinScore] = useState(0);
    const [maxScore, setMaxScore] = useState(100);
    const [isSorted, setIsSorted] = useState(false);

    const compareStudents = (a, b) => {
        if (a.score!== b.score) {
            return b.score - a.score;
        } else {
            return a.name.localeCompare(b.name);
        }
    };

    const filteredStudents = students.filter(student => {
        const nameMatch = student.name.toLowerCase().includes(filterText.toLowerCase());
        const scoreMatch = student.score >= minScore && student.score <= maxScore;
        return nameMatch && scoreMatch;
    });

    const sortedAndFilteredStudents = isSorted
      ? filteredStudents.sort(compareStudents)
       : filteredStudents;

    const handleFilterChange = (e) => {
        setFilterText(e.target.value);
    };

    const handleMinScoreChange = (e) => {
        setMinScore(parseInt(e.target.value, 10));
    };

    const handleMaxScoreChange = (e) => {
        setMaxScore(parseInt(e.target.value, 10));
    };

    const handleSort = () => {
        setIsSorted(!isSorted);
    };

    return (
        <div>
            <input
                type="text"
                placeholder="搜索学生"
                value={filterText}
                onChange={handleFilterChange}
            />
            <input
                type="number"
                placeholder="最小成绩"
                value={minScore}
                onChange={handleMinScoreChange}
            />
            <input
                type="number"
                placeholder="最大成绩"
                value={maxScore}
                onChange={handleMaxScoreChange}
            />
            <button onClick={handleSort}>
                {isSorted? '恢复顺序' : '按成绩和姓名排序'}
            </button>
            <ul>
                {sortedAndFilteredStudents.map((student, index) => (
                    <li key={index}>
                        {student.name}: {student.score}
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

在上述代码中,先对学生列表进行过滤,得到 filteredStudents。然后根据 isSorted 状态决定是否对过滤后的列表进行排序,得到 sortedAndFilteredStudents。这样就实现了排序与过滤的结合。

性能优化

避免不必要的渲染

  1. 使用 React.memo:对于展示列表的子组件,如果其 props 没有变化,我们可以使用 React.memo 来避免不必要的渲染。例如,假设我们有一个 StudentItem 组件用于展示单个学生信息:
const StudentItem = React.memo(({ student }) => (
    <li>{student.name}: {student.score}</li>
));

StudentList 组件中使用 StudentItem

import React, { useState } from'react';

const StudentItem = React.memo(({ student }) => (
    <li>{student.name}: {student.score}</li>
));

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 92 }
    ]);
    const [filterText, setFilterText] = useState('');
    const [minScore, setMinScore] = useState(0);
    const [maxScore, setMaxScore] = useState(100);
    const [isSorted, setIsSorted] = useState(false);

    const compareStudents = (a, b) => {
        if (a.score!== b.score) {
            return b.score - a.score;
        } else {
            return a.name.localeCompare(b.name);
        }
    };

    const filteredStudents = students.filter(student => {
        const nameMatch = student.name.toLowerCase().includes(filterText.toLowerCase());
        const scoreMatch = student.score >= minScore && student.score <= maxScore;
        return nameMatch && scoreMatch;
    });

    const sortedAndFilteredStudents = isSorted
      ? filteredStudents.sort(compareStudents)
       : filteredStudents;

    const handleFilterChange = (e) => {
        setFilterText(e.target.value);
    };

    const handleMinScoreChange = (e) => {
        setMinScore(parseInt(e.target.value, 10));
    };

    const handleMaxScoreChange = (e) => {
        setMaxScore(parseInt(e.target.value, 10));
    };

    const handleSort = () => {
        setIsSorted(!isSorted);
    };

    return (
        <div>
            <input
                type="text"
                placeholder="搜索学生"
                value={filterText}
                onChange={handleFilterChange}
            />
            <input
                type="number"
                placeholder="最小成绩"
                value={minScore}
                onChange={handleMinScoreChange}
            />
            <input
                type="number"
                placeholder="最大成绩"
                value={maxScore}
                onChange={handleMaxScoreChange}
            />
            <button onClick={handleSort}>
                {isSorted? '恢复顺序' : '按成绩和姓名排序'}
            </button>
            <ul>
                {sortedAndFilteredStudents.map((student, index) => (
                    <StudentItem key={index} student={student} />
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

React.memo 会浅比较 StudentItem 的 props,如果 props 没有变化,组件不会重新渲染,提升了性能。

防抖与节流

  1. 过滤输入防抖:当用户在过滤输入框中快速输入时,频繁触发过滤操作可能会导致性能问题。我们可以使用防抖函数来解决这个问题。
import React, { useState } from'react';
import debounce from 'lodash/debounce';

const StudentItem = React.memo(({ student }) => (
    <li>{student.name}: {student.score}</li>
));

const StudentList = () => {
    const [students, setStudents] = useState([
        { name: 'Alice', score: 85 },
        { name: 'Bob', score: 78 },
        { name: 'Charlie', score: 92 }
    ]);
    const [filterText, setFilterText] = useState('');
    const [minScore, setMinScore] = useState(0);
    const [maxScore, setMaxScore] = useState(100);
    const [isSorted, setIsSorted] = useState(false);

    const compareStudents = (a, b) => {
        if (a.score!== b.score) {
            return b.score - a.score;
        } else {
            return a.name.localeCompare(b.name);
        }
    };

    const filteredStudents = students.filter(student => {
        const nameMatch = student.name.toLowerCase().includes(filterText.toLowerCase());
        const scoreMatch = student.score >= minScore && student.score <= maxScore;
        return nameMatch && scoreMatch;
    });

    const sortedAndFilteredStudents = isSorted
      ? filteredStudents.sort(compareStudents)
       : filteredStudents;

    const handleFilterChange = debounce((e) => {
        setFilterText(e.target.value);
    }, 300);

    const handleMinScoreChange = (e) => {
        setMinScore(parseInt(e.target.value, 10));
    };

    const handleMaxScoreChange = (e) => {
        setMaxScore(parseInt(e.target.value, 10));
    };

    const handleSort = () => {
        setIsSorted(!isSorted);
    };

    return (
        <div>
            <input
                type="text"
                placeholder="搜索学生"
                value={filterText}
                onChange={handleFilterChange}
            />
            <input
                type="number"
                placeholder="最小成绩"
                value={minScore}
                onChange={handleMinScoreChange}
            />
            <input
                type="number"
                placeholder="最大成绩"
                value={maxScore}
                onChange={handleMaxScoreChange}
            />
            <button onClick={handleSort}>
                {isSorted? '恢复顺序' : '按成绩和姓名排序'}
            </button>
            <ul>
                {sortedAndFilteredStudents.map((student, index) => (
                    <StudentItem key={index} student={student} />
                ))}
            </ul>
        </div>
    );
};

export default StudentList;

这里使用 lodashdebounce 函数,将 handleFilterChange 函数进行防抖处理,延迟 300 毫秒执行,减少了不必要的过滤操作。

虚拟列表

  1. 大数据列表场景:当列表数据量非常大时,一次性渲染所有列表项会导致性能问题。虚拟列表技术只渲染可见区域的列表项,当用户滚动时,动态加载新的列表项。
  2. 使用 react - virtualized
    • 安装库:npm install react - virtualized
    • 示例代码:
import React, { useState } from'react';
import { List } from'react - virtualized';
import 'react - virtualized/styles.css';

const StudentList = () => {
    const [students, setStudents] = useState(Array.from({ length: 1000 }, (_, i) => ({
        name: `Student ${i + 1}`,
        score: Math.floor(Math.random() * 100)
    })));
    const [filterText, setFilterText] = useState('');
    const [minScore, setMinScore] = useState(0);
    const [maxScore, setMaxScore] = useState(100);
    const [isSorted, setIsSorted] = useState(false);

    const compareStudents = (a, b) => {
        if (a.score!== b.score) {
            return b.score - a.score;
        } else {
            return a.name.localeCompare(b.name);
        }
    };

    const filteredStudents = students.filter(student => {
        const nameMatch = student.name.toLowerCase().includes(filterText.toLowerCase());
        const scoreMatch = student.score >= minScore && student.score <= maxScore;
        return nameMatch && scoreMatch;
    });

    const sortedAndFilteredStudents = isSorted
      ? filteredStudents.sort(compareStudents)
       : filteredStudents;

    const handleFilterChange = (e) => {
        setFilterText(e.target.value);
    };

    const handleMinScoreChange = (e) => {
        setMinScore(parseInt(e.target.value, 10));
    };

    const handleMaxScoreChange = (e) => {
        setMaxScore(parseInt(e.target.value, 10));
    };

    const handleSort = () => {
        setIsSorted(!isSorted);
    };

    const rowRenderer = ({ index, key, style }) => {
        const student = sortedAndFilteredStudents[index];
        return (
            <div key={key} style={style}>
                {student.name}: {student.score}
            </div>
        );
    };

    return (
        <div>
            <input
                type="text"
                placeholder="搜索学生"
                value={filterText}
                onChange={handleFilterChange}
            />
            <input
                type="number"
                placeholder="最小成绩"
                value={minScore}
                onChange={handleMinScoreChange}
            />
            <input
                type="number"
                placeholder="最大成绩"
                value={maxScore}
                onChange={handleMaxScoreChange}
            />
            <button onClick={handleSort}>
                {isSorted? '恢复顺序' : '按成绩和姓名排序'}
            </button>
            <List
                height={400}
                rowCount={sortedAndFilteredStudents.length}
                rowHeight={30}
                rowRenderer={rowRenderer}
                width={300}
            />
        </div>
    );
};

export default StudentList;

在上述代码中,使用 react - virtualizedList 组件,通过 rowRenderer 函数定义每个列表项的渲染方式。只渲染可见区域的列表项,大大提升了大数据列表的性能。

常见问题与解决方案

排序与过滤结果不符合预期

  1. 原因分析:可能是比较函数或过滤条件的逻辑错误。例如,比较函数中条件判断错误,或者过滤条件没有正确匹配数据。
  2. 解决方案:仔细检查比较函数和过滤条件的逻辑。可以通过在关键位置添加 console.log 输出调试信息,查看数据在排序和过滤过程中的变化。比如,在比较函数中输出比较的两个元素,在过滤条件中输出不符合条件的元素,以便找出问题所在。

组件性能问题

  1. 原因分析:除了前面提到的避免不必要渲染、防抖节流和虚拟列表相关问题外,频繁的状态更新也可能导致性能问题。例如,在一个循环中多次更新状态,而不是批量更新。
  2. 解决方案:使用 useReducer 代替 useState 进行复杂状态管理,它可以更好地控制状态更新逻辑,实现批量更新。另外,确保在 useEffect 中正确依赖状态,避免不必要的副作用执行。例如,如果 useEffect 依赖了一个不会改变的变量,应将其从依赖数组中移除。

数据更新未触发视图更新

  1. 原因分析:在 React 中,只有当状态发生变化时,组件才会重新渲染。如果数据更新了,但没有通过 setStateuseState 更新状态,视图不会更新。另外,如果对象或数组的引用没有改变,即使内部数据改变了,React 也可能不会检测到变化。
  2. 解决方案:确保通过 setStateuseState 来更新状态。对于对象和数组的更新,要创建新的引用。例如,使用 ... 展开运算符来创建新的数组或对象,而不是直接修改原有的对象或数组。如 setStudents([...students, newStudent]) 而不是 students.push(newStudent)

通过以上对 React 列表排序与过滤的详细介绍、代码示例以及性能优化和常见问题解决,开发者可以更好地在 React 应用中实现高效、可靠的列表排序与过滤功能,提升用户体验。