将扁平菜单转为树形结构需两次遍历:首次用对象缓存节点并初始化children,识别根节点;第二次将非根节点挂载到父节点children下,时间复杂度O(n)。
后端返回的菜单数据通常是扁平结构的数组,每个对象带
、
(或
)、
等字段。要渲染侧边栏、导航栏或树形选择器,需将其转为嵌套的树形结构。核心思路是:**用 Map 或对象缓存所有节点,再遍历一次,把子节点挂到对应父节点的
数组下**。
识别根节点并建立映射表
先遍历一次原始数组,区分根节点(如
、
或
)和普通节点。同时用 JavaScript 对象(或
)以
为键缓存每个节点,方便后续快速查找父节点:
推荐用普通对象
,键为字符串化
(如
),简单高效
注意检查
的实际值——有些接口用
字符串,有些用数字
,有些用
,需统一判断逻辑
给每个节点初始化空
,避免后续 push 时报错
单次遍历挂载子节点
再次遍历数组,对每个节点:
若它是根节点(如
),推入结果数组
否则,查出其父节点(
),将当前节点
到父节点的
中
这个过程不递归、不嵌套循环,时间复杂度稳定为 O(n),适合千级以内菜单数据。
处理多级与排序需求
如果菜单要求按层级顺序展示(如一级菜单在前,其子菜单紧随其后),可在生成树后做一次 DFS 遍历展平并记录层级;若需前端控制展开/选中状态,建议在转换时就注入额外字段,例如:
表示根,每深入一层 +1
记录从根到当前节点的 id 路径,便于高亮或定位
标记是否为叶子节点,简化模板判断
兼容性与边界情况提醒
真实项目中常见坑点:
数据存在环引用(A 的 parent 是 B,B 的 parent 是 A)——转换前可加深度限制或 visited 集合检测
父节点在数组中出现在子节点之后——没关系,因为用的是映射表查找,不是顺序依赖
id 或 parentId 类型不一致(字符串 vs 数字)——建议统一转为字符串比较,或使用宽松相等
(但需确认业务允许)
后端返回空数组或 null —— 转换函数开头加守卫:
idparentIdpidnamechildrenparentId === nullparentId === 0parentId === undefinedMapid{}idmap[item.id] = itemparentId"0"0nullchildren: []!item.parentId || item.parentId === '0' || item.parentId === 0map[item.parentId]pushchildrenlevel: 0path: ['1', '5', '12']isLeaf: !item.children?.length==if (!Array.isArray(list) || list.length === 0) return []