TypeScript 数组对象转树形结构支持无限层级转换封装通用的函数

1,038 阅读3分钟

我们在项目中,经常会遇到树形结构功能,比如部门、分类、菜单权限等模块。经常会遇到list转树形结构、树形结构转list

当列表中有多个根节点时,将其转换为树形结构并不适合使用递归,因为递归需要遍历每个节点,并且每次递归调用都需要创建一个新的函数调用帧,这可能会导致栈溢出或内存占用过高。

一种更有效的方法是使用迭代和哈希表来遍历列表,并在哈希表中保存每个节点的引用。以下是一个 TypeScript 函数,它将包含 idparentIdname 属性的列表转换为树形结构,支持多个根节点:

/**
 * 声明一个树形结构接口
 */
interface TreeNode {
  id: number;
  children?: TreeNode[];
  parentId: number;
  name: string;
}

/**
 * 后端返回list数据接口
 */
interface ListItem {
  id: number;
  parentId: number;
  name: string;

}
/**
 * list转树形结构方法
 * @param items 
 * @returns 
 */
function listToTree(items: ListItem[]): TreeNode[] {
  /**
   * 定义一个树形节点数组
   */
  const result: TreeNode[] = [];
  /**
   * 定义map key是ListItem 数组对象元素id
   * value 是ListItem 数组对象元素
   */
  const itemMap: Record<number, TreeNode> = {};

  // 遍历数组所有数组,把id当作key,把item当作valu put到 itemMap里面
  for (const item of items) {
    /**
     * 对象结构
     * 将 item 对象的属性浅拷贝到一个新对象 node 中,并将 node 对象的 children 属性设置为空数组。
     */
    const node: TreeNode = { ...item, children: [] };
    itemMap[item.id] = node;
  }

  /**
   * 遍历数组,找到它的父类
   */
  for (const item of items) {
    const node = itemMap[item.id];
    /**
     * 判断当前元素是否是根节点
     */
    if (item.parentId === 0) {
      result.push(node);
    } else {
      /**
       * 不是跟节点,通过map找他的父节点
       * 把元素添加到children
       */
      const parent = itemMap[item.parentId];
      parent.children?.push(node);
    }
  }

  return result;
}
/**
 * mock 
 * 模拟后端返回的数据
 */
const items: ListItem[] = [
  { id: 1, parentId: 0, name: '0-1'},
  { id: 2, parentId: 1, name: '1-2'},
  { id: 3, parentId: 1, name: '1-3'},
  { id: 4, parentId: 2, name: '2-4'},
  { id: 5, parentId: 2, name: '2-5'},
  { id: 6, parentId: 3, name: '3-6'},
  { id: 7, parentId: 0, name: '0-7'},
  { id: 8, parentId: 7, name: '7-8'},
  { id: 9, parentId: 7, name: '7-9'},
  { id: 10, parentId: 8, name: '8-10'},
  { id: 11, parentId: 8, name: '8-11'},
  { id: 12, parentId: 9, name: '9-12'}
];

const tree = listToTree(items);
console.log(tree);

输出结果:

[
    {
        "children": [
            {
                "children": [
                    {
                        "children": [],
                        "id": 4,
                        "name": "2-4",
                        "parentId": 2
                    },
                    {
                        "children": [],
                        "id": 5,
                        "name": "2-5",
                        "parentId": 2
                    }
                ],
                "id": 2,
                "name": "1-2",
                "parentId": 1
            },
            {
                "children": [
                    {
                        "children": [],
                        "id": 6,
                        "name": "3-6",
                        "parentId": 3
                    }
                ],
                "id": 3,
                "name": "1-3",
                "parentId": 1
            }
        ],
        "id": 1,
        "name": "0-1",
        "parentId": 0
    },
    {
        "children": [
            {
                "children": [
                    {
                        "children": [],
                        "id": 10,
                        "name": "8-10",
                        "parentId": 8
                    },
                    {
                        "children": [],
                        "id": 11,
                        "name": "8-11",
                        "parentId": 8
                    }
                ],
                "id": 8,
                "name": "7-8",
                "parentId": 7
            },
            {
                "children": [
                    {
                        "children": [],
                        "id": 12,
                        "name": "9-12",
                        "parentId": 9
                    }
                ],
                "id": 9,
                "name": "7-9",
                "parentId": 7
            }
        ],
        "id": 7,
        "name": "0-7",
        "parentId": 0
    }
]

如果想把它封装成一个通用的结构可以这样玩,通过动态传入树关联字段,从而获取对应的值


function arrayToTree<T>(
    items: T[],
  options: {
    idKey: string;
    parentKey: string;
    nameKey: string;
  }
): TreeNode[] {
  const { idKey, parentKey,nameKey } = options;
  console.log(idKey,parentKey,nameKey)
 const result: TreeNode[] = [];
 const itemMap: Record<string, TreeNode> = {};
 for (const item of items) {
   const node:TreeNode ={
    uid:item[idKey],
    pid:item[parentKey],
    name:item[nameKey],
    children:[]
   } 
   itemMap[item[idKey]] = node;

 }
console.log(itemMap)
 for (const key in itemMap) {
    const node=itemMap[key]
    if(node.pid === '0'){
        result.push(node)
    }else{
        const parent=itemMap[node.pid]
        if(parent){
            parent.children?.push(node)
        }
    }
}
 return result;
}

/**
 * 声明一个树形结构接口
 */
interface TreeNode {
    uid: string;
    children?: TreeNode[];
    pid: string;
    name: string;
  }
interface ListData{
    id:string;
    parentId:string;
    name:string;
}
// 例子用法
const data:ListData[] = [
  { id: '1', parentId: '0', name: 'Node 1' },
  { id: '2', parentId: '1', name: 'Node 1.1' },
  { id: '3', parentId: '1', name: 'Node 1.2' },
  { id: '4', parentId: '2', name: 'Node 1.1.1' },
];

const tree = arrayToTree<ListData>(data, {
  idKey: 'id',
  parentKey: 'parentId',
  nameKey:'name'

});

console.log(JSON.stringify(tree));