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 在实际开发中的应用,并写出更高效、更健壮的过滤逻辑。如有疑问,欢迎交流探讨!

相关推荐
汪汪大队u1 小时前
Zabbix 6.0 部署踩坑记:从启动失败到 Web 成功访问
前端·zabbix
赵大仁1 小时前
Next.js + Vercel AI SDK 实战:30 分钟搭出流式 Chat 页面
前端·ai·实战·react·next.js·vercel
云空2 小时前
《Three.js 完整版3D魔方:带贴纸+中心固定+自动求解复原》
前端·javascript·3d·three.js
酸梅果茶2 小时前
【7】lightning_lm项目-LIO 前端 -IVox 局部地图
前端·slam
gis开发之家2 小时前
《Vue3 从入门到大神33篇》Vue3 源码详解(三):响应式核心思想——从 Object.defineProperty 到 Proxy
前端·javascript·vue.js·vue3·vue3源码
Hyyy2 小时前
流式渲染的基础——SSE
前端·后端·面试
Csvn2 小时前
📐 Layout Thrashing——一个 `offsetHeight` 让你的页面卡成 PPT
前端
qq_529599383 小时前
react js循环滚动
前端·javascript·react.js
索西引擎3 小时前
【React】严格模式:开发阶段质量保障的静态分析机制
前端·react.js·前端框架