搜索排序算法实战:从原理到性能优化的完整指南

慕容剑博 交互 阅读 2,165
赞 18 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

做前端这些年,处理搜索排序功能踩过不少坑。最开始我以为不就是个 sort() 的事嘛,结果线上用户一多、数据一杂,各种诡异问题就来了。后来我总结出一套自己用着顺手的写法,稳定、可维护,也不容易出岔子。

搜索排序算法实战:从原理到性能优化的完整指南

核心思路就一点:排序逻辑必须和 UI 完全解耦。别把排序函数直接塞进 React 组件里,也别在 Vue 的 computed 里硬写比较逻辑。我一般会把排序抽成一个纯函数,单独放一个文件里,比如叫 searchSorter.js。这样测试方便,复用也简单。

举个实际例子:用户在商品列表页搜“手机”,然后想按价格从低到高排。我不会在组件里直接写:

items.sort((a, b) => a.price - b.price)

而是这样:

// searchSorter.js
export const sortByPrice = (list, order = 'asc') => {
  return [...list].sort((a, b) => {
    if (order === 'asc') return a.price - b.price;
    return b.price - a.price;
  });
};

// 在组件里调用
const sortedItems = sortByPrice(originalItems, currentSortOrder);

注意这里我用了 [...list] 做浅拷贝。这点特别重要!千万别直接修改原始数据。我之前就因为没拷贝,导致用户切换排序后,再点回“默认排序”时数据已经乱了——因为原始数组被 sort() 搞脏了。折腾半天才发现是这个低级错误。

这几种错误写法,别再踩坑了

下面这些是我见过(也自己写过)的典型反面案例,列出来给大家避雷:

  • 在 render 或 template 里直接调用 sort():比如 React 的 JSX 里写 {items.sort(...).map()}。这会导致每次渲染都重新排序,性能差不说,还可能触发无限循环(如果排序依赖 state,而 state 又受渲染影响)。
  • 忽略数据类型:比如价格字段其实是字符串 “100”、”20″,直接相减会得到 NaN。或者时间字段是 ISO 字符串,没转成 Date 对象就比大小。这种问题本地测试没问题,一上线用户数据五花八门,立马崩。
  • 排序逻辑和过滤逻辑混在一起:比如先 filter 再 sort,但把两个逻辑写在一个函数里。一旦需求变更为“先排序再过滤”(虽然少见,但真有客户提过),就得大改。我建议拆成两个独立步骤:filter → sort,每一步都可插拔。
  • 前端排序代替后端排序:当数据量超过 100 条,尤其是带分页的时候,还在前端全量排序?别闹了。这时候应该让后端支持排序参数,前端只负责传参。我曾经在一个项目里硬扛 5000 条数据前端排序,页面卡成 PPT,老板差点把我开了。

还有一个隐藏坑:localeCompare。如果你要按中文标题排序,别用 a.title > b.title,得用 a.title.localeCompare(b.title, 'zh-CN')。不然“阿”和“啊”谁在前都说不准,不同浏览器还不一致。这问题我在线上被用户投诉过三次,才长记性。

实际项目中的坑

最近一个项目,需求是支持“综合排序”:先按是否 VIP 排,再按评分排,最后按发布时间排。听起来复杂,其实拆解清楚就行。我的做法是写一个通用排序函数,接收一个“排序规则数组”:

export const multiSort = (list, rules) => {
  return [...list].sort((a, b) => {
    for (const rule of rules) {
      const { key, order = 'asc', type = 'number' } = rule;
      let valA = a[key];
      let valB = b[key];

      // 处理 null/undefined
      if (valA == null && valB == null) continue;
      if (valA == null) return order === 'asc' ? 1 : -1;
      if (valB == null) return order === 'asc' ? -1 : 1;

      // 类型转换
      if (type === 'number') {
        valA = parseFloat(valA);
        valB = parseFloat(valB);
      } else if (type === 'date') {
        valA = new Date(valA).getTime();
        valB = new Date(valB).getTime();
      }

      let cmp = 0;
      if (type === 'string') {
        cmp = valA.localeCompare(valB, 'zh-CN');
      } else {
        cmp = valA - valB;
      }

      if (cmp !== 0) {
        return order === 'asc' ? cmp : -cmp;
      }
    }
    return 0;
  });
};

// 使用
const sorted = multiSort(items, [
  { key: 'isVip', order: 'desc', type: 'number' },
  { key: 'rating', order: 'desc', type: 'number' },
  { key: 'publishTime', order: 'desc', type: 'date' }
]);

这套方案的好处是灵活,加新排序维度不用动主逻辑。但要注意:规则顺序很重要,而且每个字段的类型必须明确标注,不然数字字符串和纯数字混在一起会出鬼。

另外,如果数据来自 API,记得在请求时带上排序参数。比如:

const params = new URLSearchParams({
  q: keyword,
  sort: 'price',
  order: 'asc'
});
fetch(https://jztheme.com/api/products?${params});

这样后端返回的就是已排序的数据,前端直接展示即可,省事又高效。只有在需要“二次排序”(比如用户在前端临时切换排序方式)时,才用前端排序。但这种情况要控制数据量,超过 200 条就考虑分页或懒加载。

结尾唠叨两句

以上是我这几年搞搜索排序积累的一些经验。说到底,没有银弹,关键看场景。小数据量、交互频繁的,前端排序更灵活;大数据量、稳定排序的,交给后端更靠谱。我现在的项目基本都是前后端配合:默认排序由后端处理,前端只提供有限的“轻量级”排序选项(比如只排当前页)。

还有个小细节:排序状态一定要体现在 URL 里。比如 ?sort=price&order=asc,这样用户刷新页面或分享链接,排序不会丢。这个我一开始嫌麻烦没做,结果产品天天追着问“为什么用户说排序失效了”……

以上是我个人对搜索排序的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合防抖优化性能),后续会继续分享这类博客。希望我的踩坑经验能帮你少走点弯路。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论