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. 权限继承(子节点权限变更时更新父节点)树形组件综合示例
包含了traverseTree和traverseTreeUpward,traverseTreeToArray综合使用
loading