人人都会AI编程

31.2 列表与分页:渲染、筛选、排序、分页

更新时间:2026-07-11

列表是前端开发中出现频率最高的 UI 模式之一,几乎每一个后台管理系统、电商页面、信息流应用都离不开它。而列表很少是静止的——它需要渲染大量数据,支持筛选与排序,还要用分页或无限滚动来控制数据量。这个小节将用最实用、最落地的方案,带你搞定“列表四件套”。


一、渲染:把数组变成界面

最基本的列表渲染就是一个循环:拿到一个数组,遍历后为每一项生成对应的 DOM 或组件。

const listData = [
  { id: 1, name: '张三', age: 28 },
  { id: 2, name: '李四', age: 32 },
  { id: 3, name: '王五', age: 24 }
];

// 原生 DOM 渲染
function renderList(data) {
  const container = document.getElementById('list-container');
  container.innerHTML = ''; // 清空
  const fragment = document.createDocumentFragment();
  data.forEach(item => {
    const div = document.createElement('div');
    div.innerHTML = `${item.name} - ${item.age}岁`;
    fragment.appendChild(div);
  });
  container.appendChild(fragment);
}

关键点:始终使用 key
在 React、Vue 等框架中,循环渲染时需要一个唯一的 key。它能帮助框架精确识别哪些元素变了,避免不必要的 DOM 操作。

// React 示例
data.map(item => <li key={item.id}>{item.name}</li>)

性能注意: 当列表数据量极大(如超过几百条)且 DOM 结构复杂时,一次性渲染所有节点可能导致页面卡顿。此时应考虑:

  • 使用文档碎片DocumentFragment)或批量更新 DOM
  • 使用虚拟滚动(只渲染可视区域内的元素,详见 31.5 节)
  • 将数据分页或滚动加载,减少单次渲染数量

二、筛选:在海量数据中精准检索

筛选通常包括“按关键字搜索”和“按条件过滤”。根据数据量大小,有两种实现策略:

2.1 前端筛选

适用于数据总量不大(比如几百条),且所有数据已一次性加载到前端的场景。

// 假设 listData 是全部数据
function filterData(keyword) {
  return listData.filter(item =>
    item.name.includes(keyword) || item.age.toString().includes(keyword)
  );
}

注意: 筛选后要重置分页到第一页,否则用户可能看到空白结果页。

2.2 后端筛选

当数据量很大时,筛选应在数据库层面完成。前端只需将筛选条件作为请求参数发给后端:

fetch(`/api/users?keyword=${keyword}`)
  .then(res => res.json())
  .then(data => renderList(data));

实际开发中:在搜索框输入时增加防抖(debounce),避免每敲一个字母都发送请求。

const handleSearch = debounce((keyword) => {
  fetchData({ keyword, page: 1 }); // 筛选项变化重置分页
}, 300);

三、排序:让数据有章可循

排序同样分为前端实现和后端实现。

3.1 前端排序

使用数组的 sort 方法,但要特别注意默认的排序顺序是将元素转换为字符串后比较,这可能导致数字排序的诡异结果。正确做法是比较函数:

// 按年龄升序
listData.sort((a, b) => a.age - b.age);

// 按姓名拼音顺序
listData.sort((a, b) => a.name.localeCompare(b.name, 'zh'));

排序状态的 UI 反馈:通常会在表头用箭头图标表示当前排序列和升降序。用一个状态变量记住当前排序列和方向。

let sortKey = 'age';
let sortOrder = 'asc'; // 'asc' or 'desc'

function sortData(data) {
  return [...data].sort((a, b) => {
    const valA = a[sortKey];
    const valB = b[sortKey];
    if (typeof valA === 'string') {
      return sortOrder === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
    }
    return sortOrder === 'asc' ? valA - valB : valB - valA;
  });
}

3.2 后端排序

将排序字段和方向作为请求参数:GET /api/users?sort=age&order=desc。后端处理完直接返回正确的数据顺序,前端只负责渲染。


四、分页:把大象装进小冰箱

当数据总量远超一个屏幕能展示的量时,分页是必须的。它也能显著减少初始渲染的 DOM 节点数,提升性能。

4.1 前端分页

全部数据已经在前端,通过计算进行切片展示。

let currentPage = 1;
const pageSize = 10;

function getPageData(data) {
  const start = (currentPage - 1) * pageSize;
  return data.slice(start, start + pageSize);
}

分页组件需要提供页码按钮、上一页、下一页,以及总页数信息。注意边界情况:小于 1 或大于总页数。

4.2 后端分页

数据由后端返回指定页码的内容和总条数,前端根据总条数计算分页器。

请求:GET /api/users?page=1&pageSize=10
响应:

{
  "data": [...],
  "total": 100
}

前端分页 vs 后端分页的选择:

  • 数据总量小(几千以内)、变化不频繁 → 前端分页,体验更快
  • 数据量大、实时性要求高 → 后端分页,节省带宽和内存

五、将四者串联起来

在实际开发中,筛选、排序、分页从来不是孤立的,它们之间需要联动:

  1. 筛选条件变化 → 重置分页到第 1 页,重新请求数据(或重新筛选)
  2. 排序方式变化 → 保持当前页码,重新排序(或重新请求)
  3. 分页切换 → 只改变页码,其他条件保持不变

一个清晰的状态管理至关重要。可以用一个对象来统一管理:

let queryParams = {
  keyword: '',
  page: 1,
  pageSize: 10,
  sortKey: 'age',
  sortOrder: 'asc'
};

function fetchData() {
  const params = new URLSearchParams(queryParams);
  fetch(`/api/users?${params}`)
    .then(res => res.json())
    .then(({ data, total }) => {
      renderList(data);
      renderPagination(total);
    });
}

无论使用原生 JavaScript 还是 Vue/React 的响应式数据,核心原则都一样:保证数据流是单向且可预测的


六、常见陷阱与优化

  • 筛选或排序后没有重置分页:用户筛出一个 3 条结果的数据集,结果页码还停留在第 5 页,展示一片空白。
  • 排序不拷贝原数组直接调用 sortsort 会修改原数组,这可能影响后续的其他操作。一定要用 [...data].sort(...)data.slice().sort(...)
  • 没有处理加载状态和空状态:数据请求过程中显示 loading,结果为空显示“暂无数据”,这是最基本的用户体验。
  • 分页组件复杂度过高:当总页数上百时,页码按钮太多反而难用,常见的做法是只展示部分页码(如首尾和当前页附近,中间用省略号)。

掌握了这些核心的列表操作模式,你就能应对 90% 以上的业务场景。剩下的,就是根据具体产品需求,在用户体验和性能之间做细微的权衡。