树形结构转成列表(处理菜单)

将嵌套的树形结构数据扁平化为一维数组,常用于菜单权限处理、数据展示等场景

问题

在前端开发中,我们经常会遇到树形结构的数据(如菜单、组织架构等),这些数据通常具有父子嵌套关系。但在某些场景下,我们需要将树形结构转换为扁平的列表结构,比如:

  • 权限校验时需要获取所有菜单项
  • 搜索功能需要遍历所有节点
  • 数据导出需要扁平化处理
  • 面包屑导航需要查找路径

本题要求实现一个函数,将树形结构的菜单数据转换为一维数组列表。

解答

方法一:递归实现(深度优先)

/**
 * 树形结构转列表 - 递归实现
 * @param {Array} tree - 树形结构数据
 * @param {String} childrenKey - 子节点的键名,默认为 'children'
 * @returns {Array} 扁平化后的列表
 */
function treeToList(tree, childrenKey = 'children') {
  const result = [];
  
  function traverse(nodes, parent = null) {
    nodes.forEach(node => {
      // 提取当前节点(不包含 children)
      const { [childrenKey]: children, ...item } = node;
      
      // 可选:添加父节点信息
      if (parent) {
        item.parentId = parent.id;
      }
      
      result.push(item);
      
      // 递归处理子节点
      if (children && children.length > 0) {
        traverse(children, node);
      }
    });
  }
  
  traverse(tree);
  return result;
}

方法二:迭代实现(广度优先)

/**
 * 树形结构转列表 - 迭代实现
 * @param {Array} tree - 树形结构数据
 * @param {String} childrenKey - 子节点的键名,默认为 'children'
 * @returns {Array} 扁平化后的列表
 */
function treeToListIterative(tree, childrenKey = 'children') {
  const result = [];
  const queue = [...tree]; // 使用队列存储待处理节点
  
  while (queue.length > 0) {
    const node = queue.shift();
    const { [childrenKey]: children, ...item } = node;
    
    result.push(item);
    
    // 将子节点加入队列
    if (children && children.length > 0) {
      queue.push(...children);
    }
  }
  
  return result;
}

方法三:保留层级信息

/**
 * 树形结构转列表 - 保留层级和路径信息
 * @param {Array} tree - 树形结构数据
 * @param {String} childrenKey - 子节点的键名
 * @returns {Array} 扁平化后的列表(包含层级和路径信息)
 */
function treeToListWithLevel(tree, childrenKey = 'children') {
  const result = [];
  
  function traverse(nodes, level = 0, path = []) {
    nodes.forEach(node => {
      const { [childrenKey]: children, ...item } = node;
      
      // 添加层级和路径信息
      const currentPath = [...path, item.id];
      result.push({
        ...item,
        level,           // 层级深度
        path: currentPath, // 从根到当前节点的路径
        pathStr: currentPath.join('/') // 路径字符串
      });
      
      // 递归处理子节点
      if (children && children.length > 0) {
        traverse(children, level + 1, currentPath);
      }
    });
  }
  
  traverse(tree);
  return result;
}

使用示例

// 示例数据:菜单树形结构
const menuTree = [
  {
    id: 1,
    name: '系统管理',
    path: '/system',
    children: [
      {
        id: 11,
        name: '用户管理',
        path: '/system/user',
        children: [
          { id: 111, name: '用户列表', path: '/system/user/list' },
          { id: 112, name: '添加用户', path: '/system/user/add' }
        ]
      },
      {
        id: 12,
        name: '角色管理',
        path: '/system/role'
      }
    ]
  },
  {
    id: 2,
    name: '内容管理',
    path: '/content',
    children: [
      { id: 21, name: '文章管理', path: '/content/article' },
      { id: 22, name: '分类管理', path: '/content/category' }
    ]
  }
];

// 基础转换
const list1 = treeToList(menuTree);
console.log('基础转换:', list1);
// 输出: [
//   { id: 1, name: '系统管理', path: '/system' },
//   { id: 11, name: '用户管理', path: '/system/user', parentId: 1 },
//   { id: 111, name: '用户列表', path: '/system/user/list', parentId: 11 },
//   ...
// ]

// 迭代方式转换
const list2 = treeToListIterative(menuTree);
console.log('迭代转换:', list2);

// 保留层级信息
const list3 = treeToListWithLevel(menuTree);
console.log('带层级信息:', list3);
// 输出: [
//   { id: 1, name: '系统管理', path: '/system', level: 0, path: [1], pathStr: '1' },
//   { id: 11, name: '用户管理', path: '/system/user', level: 1, path: [1, 11], pathStr: '1/11' },
//   ...
// ]

// 实际应用:获取所有菜单路径
const allPaths = treeToList(menuTree).map(item => item.path);
console.log('所有路径:', allPaths);

// 实际应用:查找特定菜单
const flatList = treeToList(menuTree);
const targetMenu = flatList.find(item => item.id === 111);
console.log('查找结果:', targetMenu);

关键点

  • 递归 vs 迭代:递归实现代码简洁直观,迭代实现避免了深层嵌套可能导致的栈溢出问题

  • 解构赋值:使用 const { children, ...item } = node 优雅地分离子节点和其他属性

  • 灵活的子节点键名:通过参数 childrenKey 支持不同的数据结构(如 childrensubMenu 等)

  • 保留关键信息:根据业务需求可以保留父节点 ID、层级深度、路径等信息,便于后续处理

  • 性能考虑:对于大量数据,迭代方式更安全;递归方式更适合中小规模数据

  • 边界处理:需要判断 children 是否存在且为数组,避免空指针错误

  • 不可变性:使用扩展运算符创建新对象,避免修改原始数据结构