
本文介绍在 next.js 13 管理后台开发中,如何使用 es6+ 原生 javascript 将扁平的字符串二维数组(含父子关系标识)高效构建为具有 `children` 层级结构的对象数组,适用于菜单、分类、组织架构等树形数据渲染场景。
在构建管理后台的侧边栏菜单、产品分类或组织架构树时,后端常返回扁平化的二维数组(如 [parentID, id, name]),而前端组件(如 Ant Design 的 Tree、MUI 的 TreeView 或自定义递归组件)需要的是嵌套的树形对象结构。直接使用 map() 仅能做线性映射,无法建立父子引用关系——这正是问题的核心难点。
解决思路是:先建立全局 ID → 节点对象的映射表(Map),再按父子关系动态挂载 children,最后筛选出根节点(parentID === "")作为顶层入口。该方法时间复杂度为 O(n),无需递归,稳定高效。
以下是完整实现:
function buildTreeFromFlatArray(data: string[][]): Array<{ id: number; name: string; children: any[] }> {
const map = new Map();
// 第一步:遍历所有项,初始化每个节点(id 为 key),并存入 map
for (const [parentId, id, name] of data) {
map.set(id, {
id: parseInt(id, 10),
name,
children: [],
});
}
// 第二步:建立父子关系 —— 将子节点 push 到对应父节点的 children 中
for (const [parentId, id, name] of data) {
if (parentId !== "") {
const parent = map.get(parentId);
const child = map.get(id);
if (parent && child) {
parent.children.push(child);
}
}
}
// 第三步:收集所有根节点(parentId 为空字符串)
const roots: typeof map.values extends () => infer T ? T extends Iterable ? U[] : [] : [] : [] = [];
for (const [parentId] of data) {
if (parentId === "") {
const root = map.get(data.find(row => row[0] === "")?.[1] || "");
if (root) roots.push(root);
}
}
// 更健壮的根节点提取方式(推荐):
const result: typeof roots = [];
for (const row of data) {
if (row[0] === "") {
const root = map.get(row[1]);
if (root) result.push(root);
}
}
return result;
} ✅ 关键要点说明:
- 使用 Map 而非普通对象,避免字符串 id(如 "01")被误转为数字键导致冲突;
- parseInt(id, 10) 显式指定十进制,防止前导零引发八进制解析错误;
- 依赖输入顺序:父节点必须出现在其子节点之前(如 ["", "1", "..."] 需在 ["1", "2", "..."] 之前)。若顺序不可控,需两轮遍历(第一轮建节点,第二轮补 children),本实现已满足该前提;
- children 初始化为空数组,确保类型安全,便于后续 push 操作;
- 最终返回的是独立的根节点数组,每个根及其全部后代均为引用关系,结构完整可直接用于 React 渲染。
? 扩展建议(Next.js 场景):
在 getServerSideProps 或 generateStaticParams 中调用此函数预处理数据,避免客户端重复计算;若数据量极大,可配合 useMemo 缓存结果;如需支持多层级深度限制或循环检测,可在第二步加入 visited 标记逻辑。
该方案简洁、可读性强、无外部依赖,完美适配现代 TypeScript/Next.js 工程实践。










