Skip to content

arrayToTree

将扁平数组结构转换为树形结构,支持自定义字段名和节点处理回调。

arrayToTree函数类型

typescript
function arrayToTree<T extends Record<string, any>, R = T>(
  arrayData: T[],
  callback?: (node: T, level: number, parent?: R) => R,
  keyConfig?: KeyConfig
): R[]

interface KeyConfig {
  idKey?: string;         // 节点 id 字段名,默认 'id'
  childrenKey?: string;   // 子节点字段名,默认 'children'
  parentIdKey?: string;   // 父节点 id 字段名,默认 'parentId'
}

泛型参数说明:

  • T: 输入的扁平节点类型,必须是对象类型
  • R: 输出的树节点类型,默认为 T。当需要为节点添加额外属性时,需显式指定

参数说明:

  • arrayData: 扁平数组数据,类型为 T[]
  • callback: 节点处理回调,入参为当前节点(类型 T)、层级(number)、父节点(类型 R | undefined),返回处理后的节点(类型 R
  • keyConfig: 字段名配置,用于自定义节点的 id、parentId、children 字段名
  • 返回值: 树形结构数组,类型为 R[]

类型使用建议:

typescript
// 场景1: 不添加额外属性,使用默认泛型
interface Node {
  id: number;
  name: string;
  parentId: number | null;
}
const tree = arrayToTree<Node>(data, (node) => node);

// 场景2: 添加额外属性,需要定义输出类型
interface InputNode {
  id: number;
  name: string;
  parentId: number | null;
}

interface OutputNode extends InputNode {
  level: number;
  parentName: string | null;
  isLeaf: boolean;
  children: OutputNode[];  // 注意:children 类型必须是 OutputNode[]
}

const tree = arrayToTree<InputNode, OutputNode>(
  data,
  (node, level, parent) => ({
    ...node,
    level,
    parentName: parent?.name || null,
    isLeaf: false,  // 初始值,会在构建完成后更新
    children: []    // 会被自动填充
  })
);
使用示例
typescript
// 定义输入和输出类型
interface FlatNode {
  id: number;
  name: string;
  parentId: number | null;
}

interface TreeNodeWithMeta extends FlatNode {
  level: number;
  parentName: string | null;
  isLeaf: boolean;
  children: TreeNodeWithMeta[];
}

// 假设有如下扁平数据:
const flatData: FlatNode[] = [
  { id: 1, name: '根节点', parentId: null },
  { id: 2, name: '一级-1', parentId: 1 },
  { id: 3, name: '一级-2', parentId: 1 },
  { id: 4, name: '二级-1-1', parentId: 2 },
  { id: 5, name: '二级-1-2', parentId: 2 },
  { id: 6, name: '二级-2-1', parentId: 3 },
  { id: 7, name: '孤立节点', parentId: null }
];

// 转换为树结构,并在回调中添加层级、父节点名、是否为叶子节点
const tree = arrayToTree<FlatNode, TreeNodeWithMeta>(
  flatData,
  (node, level, parent) => ({
    ...node,
    level,
    parentName: parent?.name || null,
    isLeaf: !(node as any).children || (node as any).children.length === 0,
    children: [] as TreeNodeWithMeta[]
  })
);

// 支持自定义字段名
interface CustomFlatNode {
  uid: string;
  label: string;
  pid: string | null;
}

interface CustomTreeNode extends CustomFlatNode {
  level: number;
  nodes: CustomTreeNode[];
}

const customFlatData: CustomFlatNode[] = [
  { uid: 'a', label: 'A', pid: null },
  { uid: 'b', label: 'B', pid: 'a' }
];

const customTree = arrayToTree<CustomFlatNode, CustomTreeNode>(
  customFlatData,
  (node, level) => ({ ...node, level, nodes: [] as CustomTreeNode[] }),
  { idKey: 'uid', parentIdKey: 'pid', childrenKey: 'nodes' }
);

// tree 结果结构如下:
// [
//   {
//     id: 1,
//     name: '根节点',
//     parentId: null,
//     level: 0,
//     parentName: null,
//     isLeaf: false,
//     children: [
//       {
//         id: 2,
//         name: '一级-1',
//         parentId: 1,
//         level: 1,
//         parentName: '根节点',
//         isLeaf: false,
//         children: [ ... ]
//       },
//       ...
//     ]
//   },
//   {
//     id: 7,
//     name: '孤立节点',
//     parentId: null,
//     level: 0,
//     parentName: null,
//     isLeaf: true,
//     children: []
//   }
// ]

traverseTree

递归遍历树形结构,支持自定义字段名和泛型约束,对每个节点执行回调并返回新的树结构。

traverseTree函数类型

typescript
function traverseTree<T extends Record<string, any>, R = T>(
  treeData: T[],
  callback: (node: T, level: number) => R,
  keyConfig?: KeyConfig
): R[]

泛型参数说明:

  • T: 输入的树节点类型,必须是对象类型
  • R: 输出的树节点类型,默认为 T。当需要为节点添加或修改属性时,需显式指定

参数说明:

  • treeData: 原始树形数组,类型为 T[]
  • callback: 节点处理回调,入参为当前节点(类型 T)、层级(number),返回处理后的节点(类型 R不包含 children 字段)
  • keyConfig: 字段名配置,用于自定义子节点字段名
  • 返回值: 处理后的树形数组,类型为 R[],children 字段会被自动递归处理

重要提示:

  • 回调函数返回的节点不需要包含 children 字段,函数会自动递归处理子节点
  • 如果输入类型 T 中有 children?: T[],输出类型 R 应该定义为 children?: R[]
  • children 字段会被自动递归转换为输出类型

类型使用建议:

typescript
// 场景1: 添加额外属性
interface InputNode {
  id: number;
  name: string;
  children?: InputNode[];
}

interface OutputNode {
  id: number;
  name: string;
  level: number;           // 新增属性
  children?: OutputNode[]; // 递归类型
}

const result = traverseTree<InputNode, OutputNode>(
  data,
  (node, level) => ({
    id: node.id,
    name: node.name,
    level  // 只返回节点自身属性,不需要处理 children
  })
);

// 场景2: 修改现有属性
interface Node {
  id: number;
  name: string;
  children?: Node[];
}

interface EnhancedNode {
  id: number;
  name: string;      // 会被修改
  label: string;     // 新增
  children?: EnhancedNode[];
}

const result = traverseTree<Node, EnhancedNode>(
  data,
  (node, level) => ({
    id: node.id,
    name: node.name.toUpperCase(),
    label: `Level ${level}: ${node.name}`
  })
);
使用示例
typescript
// 定义输入和输出类型
interface ProductNode {
  id: number;
  name: string;
  type: 'category' | 'product';
  price?: number;
  children?: ProductNode[];
}

interface ProductNodeWithMeta {
  id: number;
  name: string;
  type: 'category' | 'product';
  price?: number;
  level: number;
  label: string;
  expandable: boolean;
  icon: string;
  children?: ProductNodeWithMeta[];  // 注意:递归类型
}

// 假设有如下树形数据:
const treeData: ProductNode[] = [
  {
    id: 1,
    name: '产品',
    type: 'category',
    children: [
      { 
        id: 2, 
        name: '电子产品',
        type: 'category',
        children: [
          { id: 4, name: '手机', type: 'product', price: 5000 },
          { id: 5, name: '电脑', type: 'product', price: 8000 }
        ]
      },
      { 
        id: 3, 
        name: '家居用品',
        type: 'category',
        children: [
          { id: 6, name: '沙发', type: 'product', price: 3000 }
        ]
      }
    ]
  }
];

// 为每个节点添加层级、标签、是否可展开等属性
const result = traverseTree<ProductNode, ProductNodeWithMeta>(
  treeData,
  (node, level) => ({
    ...node,
    level,
    label: `${node.name} (${node.type})`,
    expandable: node.type === 'category',
    icon: node.type === 'category' ? '📁' : '📦'
    // 注意:不需要返回 children,会被自动处理
  })
);

// 支持自定义 children 字段名
interface MenuItem {
  id: number;
  title: string;
  items?: MenuItem[];
}

interface MenuItemWithLevel {
  id: number;
  title: string;
  level: number;
  items?: MenuItemWithLevel[];
}

const customTreeData: MenuItem[] = [
  {
    id: 1,
    title: '菜单',
    items: [
      { id: 2, title: '子菜单', items: [] }
    ]
  }
];

const customResult = traverseTree<MenuItem, MenuItemWithLevel>(
  customTreeData,
  (node, level) => ({ ...node, level }),
  { childrenKey: 'items' }
);

// result 结果结构如下(保持树形):
// [
//   {
//     id: 1,
//     name: '产品',
//     type: 'category',
//     level: 0,
//     label: '产品 (category)',
//     expandable: true,
//     icon: '📁',
//     children: [
//       {
//         id: 2,
//         name: '电子产品',
//         type: 'category',
//         level: 1,
//         label: '电子产品 (category)',
//         expandable: true,
//         icon: '📁',
//         children: [
//           {
//             id: 4,
//             name: '手机',
//             type: 'product',
//             price: 5000,
//             level: 2,
//             label: '手机 (product)',
//             expandable: false,
//             icon: '📦',
//             children: []
//           },
//           ...
//         ]
//       },
//       ...
//     ]
//   }
// ]

traverseTreeToArray

递归遍历树形结构,将所有节点展平成一维数组,支持自定义字段名和泛型约束。

traverseTreeToArray函数类型

typescript
function traverseTreeToArray<T extends Record<string, any>, R = T>(
  treeData: T[],
  callback: (node: T, level: number) => R,
  keyConfig?: KeyConfig
): R[]

泛型参数说明:

  • T: 输入的树节点类型,必须是对象类型
  • R: 输出的扁平节点类型,默认为 T。当需要为节点添加或修改属性时,需显式指定

参数说明:

  • treeData: 原始树形数组,类型为 T[]
  • callback: 节点处理回调,入参为当前节点(类型 T)、层级(number),返回处理后的节点(类型 R
  • keyConfig: 字段名配置,用于自定义子节点字段名
  • 返回值: 展平后的节点数组,类型为 R[],按深度优先顺序排列

重要提示:

  • 返回的数组是按深度优先(先序遍历)顺序排列
  • 每个节点都会作为独立项出现在数组中,原有的树形结构被展平
  • 适用于需要将树形数据转为列表形式的场景(如虚拟滚动、索引构建等)

类型使用建议:

typescript
// 场景1: 添加扁平化所需的属性
interface TreeNode {
  id: number;
  name: string;
  type: string;
  children?: TreeNode[];
}

interface FlatNode {
  id: number;
  name: string;
  type: string;
  level: number;    // 节点层级
  index: number;    // 节点索引
  isLeaf: boolean;  // 是否叶子节点
  // 注意:通常不需要 children 字段,因为已经展平
}

let index = 0;
const flatArray = traverseTreeToArray<TreeNode, FlatNode>(
  data,
  (node, level) => ({
    id: node.id,
    name: node.name,
    type: node.type,
    level,
    index: index++,
    isLeaf: !node.children || node.children.length === 0
  })
);

// 场景2: 保留原始数据并添加额外信息
interface OrgNode {
  id: number;
  name: string;
  position?: string;
  children?: OrgNode[];
}

interface OrgNodeFlat extends OrgNode {
  level: number;
  displayName: string;
}

const flatArray = traverseTreeToArray<OrgNode, OrgNodeFlat>(
  data,
  (node, level) => ({
    ...node,
    level,
    displayName: node.position ? `${node.name} - ${node.position}` : node.name
  })
);
使用示例
typescript
// 定义输入和输出类型
interface OrgNode {
  id: number;
  name: string;
  type: 'department' | 'employee';
  position?: string;
  children?: OrgNode[];
}

interface OrgNodeFlat {
  id: number;
  name: string;
  type: 'department' | 'employee';
  position?: string;
  level: number;
  index: number;
  isLeaf: boolean;
  displayName: string;
  // 注意:展平后通常不需要 children 字段
}

// 假设有如下树形数据:
const treeData: OrgNode[] = [
  {
    id: 1,
    name: '组织架构',
    type: 'department',
    children: [
      { 
        id: 2, 
        name: '技术部',
        type: 'department',
        children: [
          { id: 4, name: '张三', type: 'employee', position: '前端工程师' },
          { id: 5, name: '李四', type: 'employee', position: '后端工程师' }
        ]
      },
      { 
        id: 3, 
        name: '市场部',
        type: 'department',
        children: [
          { id: 6, name: '王五', type: 'employee', position: '市场专员' }
        ]
      }
    ]
  }
];

// 展平为数组,并为每个节点添加层级、索引、是否为叶子节点等属性
let index = 0;
const flatArray = traverseTreeToArray<OrgNode, OrgNodeFlat>(
  treeData,
  (node, level) => ({
    ...node,
    level,
    index: index++,
    isLeaf: node.type === 'employee',
    displayName: node.type === 'employee' 
      ? `${node.name} - ${node.position}` 
      : node.name
  })
);

// 支持自定义 children 字段名
interface FileNode {
  id: number;
  title: string;
  items?: FileNode[];
}

interface FileNodeWithLevel {
  id: number;
  title: string;
  level: number;
}

const customTreeData: FileNode[] = [
  {
    id: 1,
    title: '文件夹',
    items: [
      { id: 2, title: '文档', items: [] }
    ]
  }
];

const customFlatArray = traverseTreeToArray<FileNode, FileNodeWithLevel>(
  customTreeData,
  (node, level) => ({ ...node, level }),
  { childrenKey: 'items' }
);

// flatArray 结果结构如下(按先序遍历展平):
// [
//   {
//     id: 1,
//     name: '组织架构',
//     type: 'department',
//     level: 0,
//     index: 0,
//     isLeaf: false,
//     displayName: '组织架构',
//     children: [...]
//   },
//   {
//     id: 2,
//     name: '技术部',
//     type: 'department',
//     level: 1,
//     index: 1,
//     isLeaf: false,
//     displayName: '技术部',
//     children: [...]
//   },
//   {
//     id: 4,
//     name: '张三',
//     type: 'employee',
//     position: '前端工程师',
//     level: 2,
//     index: 2,
//     isLeaf: true,
//     displayName: '张三 - 前端工程师',
//     children: []
//   },
//   {
//     id: 5,
//     name: '李四',
//     type: 'employee',
//     position: '后端工程师',
//     level: 2,
//     index: 3,
//     isLeaf: true,
//     displayName: '李四 - 后端工程师',
//     children: []
//   },
//   {
//     id: 3,
//     name: '市场部',
//     type: 'department',
//     level: 1,
//     index: 4,
//     isLeaf: false,
//     displayName: '市场部',
//     children: [...]
//   },
//   {
//     id: 6,
//     name: '王五',
//     type: 'employee',
//     position: '市场专员',
//     level: 1,
//     index: 5,
//     isLeaf: true,
//     displayName: '王五 - 市场专员',
//     children: []
//   }
// ]

// 常见应用场景:
// 1. 统计树的总节点数
console.log(`总节点数: ${flatArray.length}`); // 6

// 2. 筛选所有员工
const employees = flatArray.filter(node => node.type === 'employee');

// 3. 查找特定层级的节点
const level1Nodes = flatArray.filter(node => node.level === 1);

traverseTreeUpward

从指定节点开始向上递归遍历树形结构,对每一层的父节点执行回调函数,直到根节点。

traverseTreeUpward函数类型

typescript
function traverseTreeUpward<T extends Record<string, any>>(
  tree: T[],
  currentNode: T,
  keyConfig?: KeyConfig,
  callback: (parentNode: T) => void
): void

interface KeyConfig {
  idKey?: string;         // 节点 id 字段名,默认 'id'
  childrenKey?: string;   // 子节点字段名,默认 'children'
  parentIdKey?: string;   // 父节点 id 字段名,默认 'parentId'
}

泛型参数说明:

  • T: 树节点的类型,必须是对象类型

参数说明:

  • tree: 完整的树形数组,类型为 T[],用于查找父节点
  • currentNode: 当前触发节点,类型为 T,作为向上遍历的起点
  • keyConfig: 字段名配置,用于自定义节点的 id、parentId、children 字段名
  • callback: 父节点处理回调,入参为父节点(类型 T),每找到一个父节点就会执行一次
  • 返回值: 无返回值(void

重要提示:

  • currentNode 开始,依次向上查找并执行回调,直到根节点(没有 parentId 的节点)
  • 回调执行顺序:从最近的父节点到最远的根节点
  • 如果 currentNode 没有 parentId 或父节点不存在,则不执行回调
  • 适用于需要向上传播状态、更新祖先节点的场景

类型使用建议:

typescript
// 场景1: 更新树形复选框的父节点状态
interface TreeNode {
  id: number;
  name: string;
  parentId?: number;
  checked: boolean;
  indeterminate: boolean;
  children?: TreeNode[];
}

const updateParentCheckState = (tree: TreeNode[], currentNode: TreeNode) => {
  traverseTreeUpward<TreeNode>(
    tree,
    currentNode,
    { idKey: 'id', parentIdKey: 'parentId', childrenKey: 'children' },
    (parentNode) => {
      // 根据子节点状态更新父节点的选中状态
      const children = parentNode.children || [];
      const checkedCount = children.filter(c => c.checked).length;
      
      if (checkedCount === 0) {
        parentNode.checked = false;
        parentNode.indeterminate = false;
      } else if (checkedCount === children.length) {
        parentNode.checked = true;
        parentNode.indeterminate = false;
      } else {
        parentNode.checked = false;
        parentNode.indeterminate = true;
      }
    }
  );
};

// 场景2: 收集从子节点到根节点的路径
interface OrgNode {
  uid: string;
  name: string;
  pid?: string;
  items?: OrgNode[];
}

const collectPath = (tree: OrgNode[], node: OrgNode) => {
  const path: string[] = [node.name];
  
  traverseTreeUpward<OrgNode>(
    tree,
    node,
    { idKey: 'uid', parentIdKey: 'pid', childrenKey: 'items' },
    (parentNode) => {
      path.unshift(parentNode.name); // 添加到路径开头
    }
  );
  
  return path.join(' > '); // 例如: "公司 > 技术部 > 前端组"
};
使用示例
typescript
// 定义树节点类型
interface CheckboxTreeNode {
  nodeId: number;
  parentNodeId?: number;
  label: string;
  checked: boolean;
  indeterminate: boolean;
  items?: CheckboxTreeNode[];
}

// 假设有如下树形数据:
const treeData: CheckboxTreeNode[] = [
  {
    nodeId: 1,
    label: '全部',
    checked: false,
    indeterminate: false,
    items: [
      {
        nodeId: 11,
        parentNodeId: 1,
        label: '技术部',
        checked: false,
        indeterminate: false,
        items: [
          {
            nodeId: 111,
            parentNodeId: 11,
            label: '前端',
            checked: false,
            indeterminate: false
          },
          {
            nodeId: 112,
            parentNodeId: 11,
            label: '后端',
            checked: false,
            indeterminate: false
          }
        ]
      },
      {
        nodeId: 12,
        parentNodeId: 1,
        label: '产品部',
        checked: false,
        indeterminate: false,
        items: [
          {
            nodeId: 121,
            parentNodeId: 12,
            label: '产品经理',
            checked: false,
            indeterminate: false
          }
        ]
      }
    ]
  }
];

// 示例1: 当子节点被选中时,向上更新所有父节点的状态
const handleNodeCheck = (
  tree: CheckboxTreeNode[],
  nodeId: number,
  checked: boolean
) => {
  // 找到并更新当前节点
  const findNode = (nodes: CheckboxTreeNode[]): CheckboxTreeNode | null => {
    for (const node of nodes) {
      if (node.nodeId === nodeId) return node;
      if (node.items) {
        const found = findNode(node.items);
        if (found) return found;
      }
    }
    return null;
  };

  const currentNode = findNode(tree);
  if (!currentNode) return;

  currentNode.checked = checked;
  currentNode.indeterminate = false;

  // 向上更新所有父节点
  traverseTreeUpward<CheckboxTreeNode>(
    tree,
    currentNode,
    {
      idKey: 'nodeId',
      parentIdKey: 'parentNodeId',
      childrenKey: 'items'
    },
    (parentNode) => {
      const children = parentNode.items || [];
      const checkedCount = children.filter((child) => child.checked).length;
      const totalCount = children.length;

      if (checkedCount === 0) {
        // 所有子节点未选中
        parentNode.checked = false;
        parentNode.indeterminate = false;
      } else if (checkedCount === totalCount) {
        // 所有子节点已选中
        const allFullyChecked = children.every(
          (child) => child.checked && !child.indeterminate
        );
        parentNode.checked = allFullyChecked;
        parentNode.indeterminate = !allFullyChecked;
      } else {
        // 部分子节点选中
        parentNode.checked = false;
        parentNode.indeterminate = true;
      }
    }
  );
};

// 模拟选中"前端"节点
handleNodeCheck(treeData, 111, true);
// 执行后,父节点状态:
// - 技术部: indeterminate=true (部分子节点选中)
// - 全部: indeterminate=true (部分子节点选中)

// 示例2: 收集从当前节点到根节点的路径
interface PathNode {
  id: number;
  name: string;
  parentId?: number;
  children?: PathNode[];
}

const pathData: PathNode[] = [
  {
    id: 1,
    name: '中国',
    children: [
      {
        id: 2,
        name: '广东省',
        parentId: 1,
        children: [
          {
            id: 3,
            name: '深圳市',
            parentId: 2,
            children: [
              { id: 4, name: '南山区', parentId: 3 }
            ]
          }
        ]
      }
    ]
  }
];

const getNodePath = (tree: PathNode[], node: PathNode): string[] => {
  const path: string[] = [node.name];

  traverseTreeUpward<PathNode>(
    tree,
    node,
    undefined,
    (parentNode) => {
      path.unshift(parentNode.name); // 添加到开头
    }
  );

  return path;
};

// 假设 node 是"南山区"节点
const nanShanNode = pathData[0].children![0].children![0].children![0];
const path = getNodePath(pathData, nanShanNode);
console.log(path); // ['中国', '广东省', '深圳市', '南山区']
console.log(path.join(' / ')); // '中国 / 广东省 / 深圳市 / 南山区'

// 示例3: 向上传播展开状态
interface ExpandableNode {
  id: number;
  label: string;
  parentId?: number;
  expanded: boolean;
  children?: ExpandableNode[];
}

const expandParents = (tree: ExpandableNode[], node: ExpandableNode) => {
  // 展开当前节点的所有父节点
  traverseTreeUpward<ExpandableNode>(
    tree,
    node,
    undefined,
    (parentNode) => {
      parentNode.expanded = true;
    }
  );
};

// 常见应用场景:
// 1. 树形复选框的父子联动
// 2. 面包屑路径生成
// 3. 自动展开父节点以显示选中的子节点
// 4. 统计祖先节点的选中数量
// 5. 权限继承(子节点权限变更时更新父节点)

树形组件综合示例

包含了traverseTreetraverseTreeUpward,traverseTreeToArray综合使用

loading