new Set 过滤数据实战

在表格过滤、多选筛选等场景中,如何高效地处理层级数据?本文通过一个真实的产品分组过滤案例,详解 Set 数据结构在数据筛选中的妙用,并给出健壮性、性能优化方案

一、背景与需求

在后台管理系统中,我们经常遇到这样的表格过滤需求:

  • 表格每一行数据关联一个 group_id(产品分组 ID)。

  • 用户可以通过下拉筛选器选择若干个分组(父级)。

  • 过滤结果需要同时包含所选分组本身及其所有子分组(子孙后代)下的数据行。

也就是说,选择"电子产品"分组时,不仅要显示 group_id 等于"电子产品"的行,还要显示其子分组(如"手机"、"电脑")下的所有行。

实现这样一个过滤逻辑,既要求代码清晰,又要兼顾性能和健壮性。

二、传统实现方式的问题

初版代码可能这样写:

javascript 复制代码
filterMethod: ({ option, row }) => {
  const selectedIds = option.data; // 假设是数组
  if (!selectedIds || selectedIds.length === 0) return true;

  // 遍历每个选中的分组,检查当前行是否属于该分组或其子分组
  return selectedIds.some(id => {
    const group = groupOptions.find(g => g.id === id);
    if (!group) return false;
    return group.id === row.group_id || 
           (group.children_id && group.children_id.includes(row.group_id));
  });
}

这种写法虽然直观,但存在几个痛点:

  • 重复查找 :每次过滤都要遍历 selectedIds,对每个 ID 还要在 groupOptions 中查找,时间复杂度高(O(n*m))。

  • 层级局限:只支持一层子分组,若存在孙级分组则无法匹配。

  • 数据安全 :未对 option.data 做类型校验,可能因数据格式异常导致报错。

三、Set 登场:化繁为简

JavaScript 的 Set 对象提供了 O(1) 的查找性能,并且天然去重。我们可以利用 Set 存储所有需要保留的分组 ID (包括选中分组及其后代),然后只需判断当前行的 group_id 是否存在于该集合中,一步到位。

优化后的代码如下(实际案例):

javascript 复制代码
filterMethod: ({ option, row }) => {
  // 1. 获取筛选数据,并进行安全处理
  const rawData = option.data;
  if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
    return true; // 无筛选条件,显示全部
  }

  // 2. 将选中的 ID 放入 Set,以便快速查找
  const selectedSet = new Set(rawData);

  // 3. 获取全部分组数据(外部响应式数据)
  const allGroups = groupOptions.value || [];

  // 4. 递归收集所有需要保留的分组 ID(包含后代)
  const keepIds = new Set();
  const collect = (id) => {
    if (keepIds.has(id)) return; // 防止重复递归
    const group = allGroups.find(g => g.id === id);
    if (!group) return;
    keepIds.add(group.id);
    if (Array.isArray(group.children_id)) {
      group.children_id.forEach(childId => collect(childId));
    }
  };

  // 对每个选中的 ID 执行收集
  rawData.forEach(id => collect(id));

  // 5. 判断当前行的 group_id 是否在 keepIds 中
  return keepIds.has(row.group_id);
}

四、代码逐段解析

1. 筛选数据安全获取

javascript 复制代码
const rawData = option.data;
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) return true;
  • 明确要求 rawData 为数组,否则视为"未选择任何条件",显示所有行。

  • 避免因 option.data 为字符串或对象时调用 new Set() 引发异常。

2. 使用 Set 存储选中 ID

javascript 复制代码
const selectedSet = new Set(rawData);
  • 虽然本例中并未直接使用 selectedSet(因为我们使用了 rawData.forEach 循环),但若后续需要快速判断某个 ID 是否被选中,Set 比数组的 includes 更高效。

  • 实际我们直接遍历 rawData 即可,但建立 Set 是一个好习惯。

3. 递归收集后代 ID

javascript 复制代码
const keepIds = new Set();
const collect = (id) => {
  if (keepIds.has(id)) return;
  const group = allGroups.find(g => g.id === id);
  if (!group) return;
  keepIds.add(group.id);
  if (Array.isArray(group.children_id)) {
    group.children_id.forEach(childId => collect(childId));
  }
};
  • 核心逻辑 :从每个选中的分组 ID 出发,递归遍历其 children_id,将所有子孙 ID 加入 keepIds

  • 利用 Set 自动去重,避免重复添加,且 has() 检查可提前终止递归(防止循环引用)。

  • 递归深度取决于分组层级,若层级过深可考虑改为迭代栈,但一般情况下足够。

4. 最终匹配

javascript 复制代码
return keepIds.has(row.group_id);
  • 只需一次 Set 查找,时间复杂度 O(1),比多层循环快得多。

五、性能与健壮性优化建议

5.1 预构建分组映射表

每次过滤都执行 allGroups.find 查找,若分组数量庞大(成千上万),可提前构建一个 Map,以 id 为键:

javascript 复制代码
const groupMap = new Map();
groupOptions.value.forEach(g => groupMap.set(g.id, g));
// 在 collect 中改为 const group = groupMap.get(id);

5.2 缓存过滤结果

如果表格数据不常变动,可以在筛选条件变化时预先计算好 keepIds,然后对每一行直接判断,减少重复计算。

5.3 处理深层级与循环引用

如果分组存在循环引用(A 的 children 包含 B,B 的 children 包含 A),上述递归会无限执行。通过 keepIds.has(id) 检查可以打破循环,因为第二次遇到相同 ID 时会直接返回。

5.4 明确数据结构

在实际业务中,应确保 groupOptions 中的每个对象都有 idchildren_id 字段(children_id 可以是空数组)。使用 TypeScript 或 JSDoc 进行类型注解,提升代码可维护性。

六、总结

  1. Set 是处理集合运算的利器:去重、判存、交集差集都极为高效,特别适合过滤、权限校验等场景。

  2. 递归展开层级结构 :通过 Set 记忆已处理节点,既能完整覆盖所有后代,又能避免死循环。

  3. 健壮性不可忽视:对输入数据做类型和空值检查,能大幅降低生产环境报错概率。

  4. 性能优化分层:对于高频操作,预先建立索引(Map/Set)可显著提升响应速度。

通过这个实战案例,我们不仅学会了如何用 Set 优雅地解决复杂过滤问题,还掌握了递归、缓存等通用优化手段。在你的下一个表格或树形筛选需求中,不妨试试这套组合拳吧!

附录:完整组件化示例(Vue 3 + Element Plus 风格)

javascript 复制代码
<script setup>
import { ref } from 'vue';

const groupOptions = ref([
  { id: 'g1', name: '电子产品', children_id: ['g2', 'g3'] },
  { id: 'g2', name: '手机', children_id: ['g4'] },
  { id: 'g3', name: '电脑', children_id: [] },
  { id: 'g4', name: '智能手机', children_id: [] },
]);

const tableData = ref([
  { group_id: 'g1', product: '电视' },
  { group_id: 'g4', product: 'iPhone' },
  { group_id: 'g3', product: 'MacBook' },
]);

const filterMethod = ({ option, row }) => {
  // ... 上述实现
};
</script>

希望这篇文章能帮助你更好地理解 Set 在实际开发中的应用,并写出更高效、更健壮的过滤逻辑。如有疑问,欢迎交流探讨!

相关推荐
AlienZHOU13 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc16 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师17 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen18 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒18 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow18 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81818 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007818 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明18 小时前
第 6 章 组件进阶
前端·vue.js
江华森18 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端