如何将三个扁平数组合并构建嵌套树形数据结构

本文介绍如何将通过多次 ajax 请求获取的三层扁平员工数据(根节点、一级子节点、二级子节点)按 `employeeid` 和父子关系动态组装为符合 treegrid 要求的嵌套树形结构,并提供可复用的递归挂载与扁平化工具函数。

在实际开发中(如使用 jqxTreeGrid、Ant Design Tree 或自定义树组件),常需将分层拉取的扁平数据构建成具有 children 属性的嵌套树结构。典型场景是:

  • 第一次请求获取顶级员工(如 EmployeeID: 2);
  • 第二次请求获取其直属下属(EmployeeID: [1,3,4,5,8]);
  • 第三次请求获取下级下属(如 EmployeeID: [6,7,9],隶属于 EmployeeID: 5)。

关键挑战在于:如何根据业务逻辑(如 ReportsTo 字段或预定义层级映射)将子数组精准挂载到对应父节点的 children 属性中? 原始数据中并未显式包含 ReportsTo 字段,因此需依赖外部约定(例如:第二层数组中的所有项均属于第一层中 EmployeeID: 2 的子节点;第三层数组中的项均属于第二层中 EmployeeID: 5 的子节点)。以下提供两种主流实现方式:

✅ 方案一:基于层级顺序 + 显式挂载(推荐用于可控数据源)

假设你已知层级归属关系(如后端返回时附带 parentId 字段),最健壮的方式是统一建模后递归挂载:

// 模拟三组异步获取的数据
const level0 = [{ EmployeeID: 2, FirstName: "Andrew", ... }]; // 根节点
const level1 = [
  { EmployeeID: 8, FirstName: "Laura", ReportsTo: 2 },
  { EmployeeID: 1, FirstName: "Nancy", ReportsTo: 2 },
  { EmployeeID: 5, FirstName: "Steven", ReportsTo: 2 }
];
const level2 = [
  { EmployeeID: 6, FirstName: "Michael", ReportsTo: 5 },
  { EmployeeID: 7, FirstName: "Robert", ReportsTo: 5 },
  { EmployeeID: 9, FirstName: "Anne", ReportsTo: 5 }
];

// 合并为单层扁平数组(含 parentId)
const allNodes = [...level0, ...level1, ...level2];

// 构建树:O(n) 时间复杂度
function buildTree(nodes, idKey = 'EmployeeID', parentKey = 'ReportsTo') {
  const nodeMap = new Map();
  const roots = [];

  // 第一遍:建立 ID → 节点映射
  nodes.forEach(node => {
    nodeMap.set(node[idKey], { ...node, children: [] });
  });

  // 第二遍:挂载子节点
  nodes.forEach(node => {
    const parentId = node[parentKey];
    const currentNode = nodeMap.get(node[idKey]);

    if (parentId == null || !nodeMap.has(parentId)) {
      roots.push(currentNode);
    } else {
      nodeMap.get(parentId).children.push(currentNode);
    }
  });

  return roots;
}

const treeData = buildTree(allNodes);
console.log(treeData); // 输出符合要求的嵌套结构
⚠️ 注意:若原始数据无 ReportsTo,需在请求时明确传递上下文(如 /api/employees?parent=2),或由前端维护映射表(如 parentMap = {2: [1,3,4,5,8], 5: [6,7,9]})。

✅ 方案二:按预设层级顺序手动组装(适用于固定结构)

若层级关系严格固定(如“第二组一定属于第一组首个元素”),可用简洁方式手动挂

载:

// 假设 level0 仅含一个根节点
if (level0.length > 0) {
  level0[0].children = level1;
  // 手动查找 level1 中 EmployeeID === 5 的节点,并挂载 level2
  const managerNode = level1.find(emp => emp.EmployeeID === 5);
  if (managerNode) {
    managerNode.children = level2;
    managerNode.expanded = "true"; // 可选:控制默认展开
  }
}
// level0 即为最终树根
const finalTree = level0;

? 辅助函数:嵌套结构 ↔ 扁平数组互转

开发调试时,常需将嵌套树转为扁平列表(如用于搜索、导出):

// 嵌套 → 扁平(广度优先)
function nestedToLinear(data) {
  const result = [];
  const queue = [...data];

  while (queue.length > 0) {
    const node = queue.shift();
    result.push({ ...node, children: undefined }); // 移除 children 避免循环引用
    if (Array.isArray(node.children) && node.children.length > 0) {
      queue.push(...node.children);
    }
  }
  return result;
}

// 使用示例
console.log(nestedToLinear(treeData)); // 输出单层数组,含全部节点

✅ 总结

  • 核心原则:树结构构建 = “建立节点索引 + 按关系挂载”,而非硬编码 children: [...];
  • 生产建议:后端应返回 id + parentId 字段,前端用 buildTree() 统一处理,避免耦合层级逻辑;
  • 性能注意:Map 查找为 O(1),整套构建为 O(n),远优于嵌套循环;
  • 扩展性:该模式天然支持 N 层嵌套,无需修改逻辑。

通过以上方法,你可灵活将任意数量的扁平数组组合为标准树形数据,无缝对接各类 TreeGrid 组件。