列表是前端开发中出现频率最高的 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 页,重新请求数据(或重新筛选)
- 排序方式变化 → 保持当前页码,重新排序(或重新请求)
- 分页切换 → 只改变页码,其他条件保持不变
一个清晰的状态管理至关重要。可以用一个对象来统一管理:
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 页,展示一片空白。
- 排序不拷贝原数组直接调用
sort:sort会修改原数组,这可能影响后续的其他操作。一定要用[...data].sort(...)或data.slice().sort(...)。 - 没有处理加载状态和空状态:数据请求过程中显示 loading,结果为空显示“暂无数据”,这是最基本的用户体验。
- 分页组件复杂度过高:当总页数上百时,页码按钮太多反而难用,常见的做法是只展示部分页码(如首尾和当前页附近,中间用省略号)。
掌握了这些核心的列表操作模式,你就能应对 90% 以上的业务场景。剩下的,就是根据具体产品需求,在用户体验和性能之间做细微的权衡。