树形工具 (tree.ts)
树结构工具函数库,提供全面的树形数据结构处理工具,用于前端各种树形数据的操作和转换。
📖 概述
树形工具库提供以下核心功能:
- 构建树:将平铺列表转换为嵌套树结构
- 查找节点:在树中查找符合条件的节点
- 查找路径:获取从根节点到目标节点的路径
- 过滤树:根据条件过滤树节点
- 扁平化:将树结构转换为一维数组
- 遍历树:遍历并对每个节点执行操作
- 插入节点:在树中指定位置插入节点
- 删除节点:移除符合条件的节点
- 更新节点:更新符合条件的节点
- 获取叶子:获取所有叶子节点
- 计算深度:获取树的最大深度
🏗️ 类型定义
TreeNode 接口
typescript
interface TreeNode {
[key: string]: any
children?: TreeNode[]
}
TreeOptions 配置选项
typescript
interface TreeOptions {
/** ID字段名称,默认为 'id' */
id?: string
/** 父ID字段名称,默认为 'parentId' */
parentId?: string
/** 子节点字段名称,默认为 'children' */
children?: string
/** 深拷贝数据,默认为true */
deepCopy?: boolean
}
🌳 构建树结构
buildTree
将平铺列表转换为嵌套树结构。
typescript
buildTree<T = TreeNode>(data: any[], options?: TreeOptions): T[]
参数:
data
- 源数据数组options
- 配置选项
返回值:
T[]
- 树结构数据
示例:
typescript
// 基本使用
const list = [
{ id: 1, name: '部门1', parentId: 0 },
{ id: 2, name: '部门2', parentId: 1 },
{ id: 3, name: '部门3', parentId: 1 },
{ id: 4, name: '部门4', parentId: 2 }
]
const tree = buildTree(list)
// 结果:
// [
// {
// id: 1, name: '部门1', parentId: 0,
// children: [
// {
// id: 2, name: '部门2', parentId: 1,
// children: [
// { id: 4, name: '部门4', parentId: 2, children: [] }
// ]
// },
// { id: 3, name: '部门3', parentId: 1, children: [] }
// ]
// }
// ]
// 自定义字段名
const customList = [
{ itemId: 1, name: '部门1', pid: 0 },
{ itemId: 2, name: '部门2', pid: 1 }
]
const customTree = buildTree(customList, {
id: 'itemId',
parentId: 'pid'
})
使用场景:
- 组织架构展示
- 菜单树构建
- 分类层级展示
- 评论回复树
🔍 查找节点
findTreeNode
在树中查找符合条件的节点。
typescript
findTreeNode<T = TreeNode>(
tree: T[],
predicate: (node: T) => boolean,
options?: TreeOptions
): T | null
参数:
tree
- 树结构数据predicate
- 判断函数options
- 配置选项
返回值:
T | null
- 找到的节点或null
示例:
typescript
const tree = buildTree(departmentList)
// 按ID查找
const node = findTreeNode(tree, node => node.id === 3)
console.log(node) // { id: 3, name: '部门3', ... }
// 按名称查找
const node2 = findTreeNode(tree, node => node.name === '研发部')
// 按条件查找
const activeNode = findTreeNode(tree, node => node.status === 'active')
findTreeNodePath
获取从根节点到目标节点的路径。
typescript
findTreeNodePath<T = TreeNode>(
tree: T[],
predicate: (node: T) => boolean,
options?: TreeOptions
): T[]
示例:
typescript
const tree = buildTree(departmentList)
// 查找ID为4的节点路径
const path = findTreeNodePath(tree, node => node.id === 4)
console.log(path)
// [
// { id: 1, name: '部门1', ... },
// { id: 2, name: '部门2', ... },
// { id: 4, name: '部门4' }
// ]
// 面包屑导航生成
const breadcrumbs = path.map(node => node.name).join(' > ')
console.log(breadcrumbs) // "部门1 > 部门2 > 部门4"
🔧 树操作
filterTree
根据条件过滤树结构。
typescript
filterTree<T = TreeNode>(
tree: T[],
predicate: (node: T) => boolean,
options?: TreeOptions
): T[]
示例:
typescript
const tree = buildTree(departmentList)
// 过滤包含"销售"的部门
const filtered = filterTree(tree, node => {
return node.name.includes('销售') ||
node.children?.some(child => child.name.includes('销售'))
})
// 过滤激活状态的节点
const activeTree = filterTree(tree, node => node.status === 'active')
flattenTree
将树结构转换为一维数组。
typescript
flattenTree<T = TreeNode>(tree: T[], options?: TreeOptions): T[]
示例:
typescript
const tree = buildTree(departmentList)
const flatArray = flattenTree(tree)
// 获取所有部门ID
const allIds = flatArray.map(item => item.id)
// 去除children字段的扁平化
const cleanArray = flatArray.map(({ children, ...rest }) => rest)
traverseTree
遍历树结构并对每个节点执行操作。
typescript
traverseTree<T = TreeNode>(
tree: T[],
callback: (node: T, parent: T | null, level: number) => void,
options?: TreeOptions
): void
示例:
typescript
const tree = buildTree(departmentList)
// 输出所有节点及其层级
traverseTree(tree, (node, parent, level) => {
const indent = ' '.repeat(level)
console.log(`${indent}${node.name} (Level ${level})`)
})
// 添加层级属性
traverseTree(tree, (node, parent, level) => {
node.level = level
node.path = parent ? `${parent.path}/${node.name}` : node.name
})
➕ 节点操作
insertNode
向树中插入节点。
typescript
insertNode<T = TreeNode>(
tree: T[],
node: T,
parentId?: any,
options?: TreeOptions
): boolean
示例:
typescript
let tree = buildTree(departmentList)
// 插入到指定父节点
const newNode = { id: 5, name: '新部门', parentId: 1 }
const success = insertNode(tree, newNode, 1)
// 插入为根节点
const rootNode = { id: 6, name: '总公司' }
insertNode(tree, rootNode, null)
removeNode
从树中删除节点。
typescript
removeNode<T = TreeNode>(
tree: T[],
predicate: (node: T) => boolean,
options?: TreeOptions
): boolean
示例:
typescript
let tree = buildTree(departmentList)
// 删除ID为3的节点
removeNode(tree, node => node.id === 3)
// 批量删除
removeNode(tree, node => [2, 3, 4].includes(node.id))
// 删除无效节点
removeNode(tree, node => !node.name || node.status === 'deleted')
updateNode
更新树节点。
typescript
updateNode<T = TreeNode>(
tree: T[],
predicate: (node: T) => boolean,
updater: (node: T) => T,
options?: TreeOptions
): boolean
示例:
typescript
let tree = buildTree(departmentList)
// 更新ID为3的节点
updateNode(
tree,
node => node.id === 3,
node => ({ ...node, name: '销售部', status: 'active' })
)
// 批量更新节点状态
updateNode(
tree,
node => node.status === 'pending',
node => ({ ...node, status: 'active', updatedAt: new Date() })
)
📊 树分析
getLeafNodes
获取树中所有叶子节点。
typescript
getLeafNodes<T = TreeNode>(tree: T[], options?: TreeOptions): T[]
示例:
typescript
const tree = buildTree(departmentList)
const leaves = getLeafNodes(tree)
// 获取所有末级部门
console.log('末级部门:', leaves.map(leaf => leaf.name))
// 统计叶子节点数量
console.log('叶子节点数量:', leaves.length)
getTreeDepth
计算树的最大深度。
typescript
getTreeDepth<T = TreeNode>(tree: T[], options?: TreeOptions): number
示例:
typescript
const tree = buildTree(departmentList)
const depth = getTreeDepth(tree)
console.log(`组织架构层级深度: ${depth}`)
// 根据深度调整UI显示
if (depth > 5) {
console.warn('层级过深,考虑优化组织结构')
}
💡 实际应用场景
1. 组织架构管理
typescript
class OrganizationManager {
private orgTree: TreeNode[]
constructor(departments: any[]) {
this.orgTree = buildTree(departments)
}
// 获取部门路径
getDepartmentPath(deptId: number): string {
const path = findTreeNodePath(this.orgTree, node => node.id === deptId)
return path.map(node => node.name).join(' > ')
}
// 获取下级部门
getSubDepartments(deptId: number): TreeNode[] {
const dept = findTreeNode(this.orgTree, node => node.id === deptId)
return dept?.children || []
}
// 获取所有员工数量
getTotalEmployeeCount(): number {
let total = 0
traverseTree(this.orgTree, (node) => {
total += node.employeeCount || 0
})
return total
}
// 重组部门
restructureDepartment(oldDeptId: number, newParentId: number) {
const dept = findTreeNode(this.orgTree, node => node.id === oldDeptId)
if (dept) {
// 从原位置删除
removeNode(this.orgTree, node => node.id === oldDeptId)
// 插入到新位置
insertNode(this.orgTree, dept, newParentId)
}
}
}
2. 菜单树管理
typescript
class MenuTreeManager {
private menuTree: TreeNode[]
constructor(menuData: any[]) {
this.menuTree = buildTree(menuData, {
id: 'menuId',
parentId: 'parentMenuId'
})
}
// 根据权限过滤菜单
filterMenuByPermission(permissions: string[]): TreeNode[] {
return filterTree(this.menuTree, node => {
return !node.permission || permissions.includes(node.permission)
})
}
// 生成面包屑导航
generateBreadcrumb(menuId: string): string[] {
const path = findTreeNodePath(this.menuTree, node => node.menuId === menuId)
return path.map(node => node.title)
}
// 获取当前菜单的兄弟菜单
getSiblingMenus(menuId: string): TreeNode[] {
const path = findTreeNodePath(this.menuTree, node => node.menuId === menuId)
if (path.length < 2) return []
const parent = path[path.length - 2]
return parent.children?.filter(child => child.menuId !== menuId) || []
}
// 菜单排序
sortMenus() {
traverseTree(this.menuTree, (node) => {
if (node.children) {
node.children.sort((a, b) => (a.sort || 0) - (b.sort || 0))
}
})
}
}
3. 文件树浏览器
typescript
class FileTreeBrowser {
private fileTree: TreeNode[]
constructor(files: any[]) {
this.fileTree = this.buildFileTree(files)
}
private buildFileTree(files: any[]): TreeNode[] {
// 构建文件路径树
const pathMap = new Map()
files.forEach(file => {
const pathParts = file.path.split('/')
let currentPath = ''
let currentLevel = pathMap
pathParts.forEach((part, index) => {
currentPath += (index > 0 ? '/' : '') + part
if (!currentLevel.has(part)) {
currentLevel.set(part, {
name: part,
path: currentPath,
isFile: index === pathParts.length - 1,
children: new Map()
})
}
currentLevel = currentLevel.get(part).children
})
})
return this.convertMapToTree(pathMap)
}
private convertMapToTree(map: Map<string, any>): TreeNode[] {
const result: TreeNode[] = []
for (const [key, value] of map) {
const node: TreeNode = {
name: value.name,
path: value.path,
isFile: value.isFile,
children: this.convertMapToTree(value.children)
}
result.push(node)
}
return result
}
// 搜索文件
searchFiles(keyword: string): TreeNode[] {
return filterTree(this.fileTree, node => {
return node.name.toLowerCase().includes(keyword.toLowerCase())
})
}
// 获取文件夹大小
getFolderSize(folderPath: string): number {
const folder = findTreeNode(this.fileTree, node => node.path === folderPath)
if (!folder || folder.isFile) return 0
let totalSize = 0
traverseTree([folder], (node) => {
if (node.isFile) {
totalSize += node.size || 0
}
})
return totalSize
}
}
4. 评论树系统
typescript
class CommentTreeSystem {
private commentTree: TreeNode[]
constructor(comments: any[]) {
this.commentTree = buildTree(comments, {
id: 'commentId',
parentId: 'replyTo'
})
}
// 添加回复
addReply(parentCommentId: number, reply: any) {
reply.replyTo = parentCommentId
reply.commentId = Date.now() // 临时ID生成
reply.children = []
insertNode(this.commentTree, reply, parentCommentId)
}
// 获取评论层级
getCommentLevel(commentId: number): number {
const path = findTreeNodePath(this.commentTree, node => node.commentId === commentId)
return path.length - 1
}
// 获取热门评论(按点赞数排序)
getHotComments(): TreeNode[] {
const allComments = flattenTree(this.commentTree)
return allComments
.filter(comment => (comment.likes || 0) > 10)
.sort((a, b) => (b.likes || 0) - (a.likes || 0))
}
// 折叠长评论串
collapseDeepComments(maxDepth: number = 5) {
traverseTree(this.commentTree, (node, parent, level) => {
if (level >= maxDepth) {
node.collapsed = true
}
})
}
// 统计回复数量
getReplyCount(commentId: number): number {
const comment = findTreeNode(this.commentTree, node => node.commentId === commentId)
if (!comment) return 0
let count = 0
traverseTree([comment], (node) => {
if (node.commentId !== commentId) {
count++
}
})
return count
}
}
🎨 Vue 组件集成
vue
<template>
<div class="tree-component">
<div
v-for="node in treeData"
:key="node.id"
class="tree-node"
>
<TreeNode
:node="node"
:level="0"
@node-click="handleNodeClick"
@node-expand="handleNodeExpand"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { buildTree, findTreeNode, updateNode } from '@/utils/tree'
// 递归树节点组件
const TreeNode = defineComponent({
name: 'TreeNode',
props: {
node: Object,
level: Number
},
emits: ['node-click', 'node-expand'],
setup(props, { emit }) {
const isExpanded = ref(true)
const hasChildren = computed(() => {
return props.node.children && props.node.children.length > 0
})
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
emit('node-expand', props.node, isExpanded.value)
}
const handleClick = () => {
emit('node-click', props.node)
}
return {
isExpanded,
hasChildren,
toggleExpand,
handleClick
}
}
})
// 主组件逻辑
interface Props {
data: any[]
options?: TreeOptions
}
const props = withDefaults(defineProps<Props>(), {
options: () => ({})
})
const treeData = computed(() => {
return buildTree(props.data, props.options)
})
const handleNodeClick = (node: any) => {
console.log('点击节点:', node)
}
const handleNodeExpand = (node: any, expanded: boolean) => {
console.log('节点展开状态:', node.name, expanded)
}
</script>
<style scoped>
.tree-node {
margin-left: v-bind('level * 20 + "px"');
}
</style>
⚡ 性能优化建议
1. 大数据量优化
typescript
// 虚拟滚动树
class VirtualTreeRenderer {
private visibleNodes: TreeNode[]
private allNodes: TreeNode[]
constructor(tree: TreeNode[]) {
this.allNodes = flattenTree(tree)
this.updateVisibleNodes()
}
updateVisibleNodes(scrollTop: number = 0, containerHeight: number = 500) {
const itemHeight = 30
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 5,
this.allNodes.length
)
this.visibleNodes = this.allNodes.slice(startIndex, endIndex)
}
getVisibleNodes(): TreeNode[] {
return this.visibleNodes
}
}
2. 懒加载
typescript
class LazyTreeLoader {
private loadedNodes = new Set<string>()
async loadChildren(nodeId: string): Promise<TreeNode[]> {
if (this.loadedNodes.has(nodeId)) {
return []
}
const children = await this.fetchChildrenFromAPI(nodeId)
this.loadedNodes.add(nodeId)
return children
}
private async fetchChildrenFromAPI(nodeId: string): Promise<TreeNode[]> {
const response = await fetch(`/api/tree-nodes/${nodeId}/children`)
return response.json()
}
}
⚠️ 注意事项
- 循环引用:确保数据中没有循环引用,可能导致无限递归
- 性能考虑:大量节点时考虑虚拟滚动或分页加载
- 内存占用:深拷贝会增加内存使用,根据需要选择
- 数据一致性:修改树结构时注意保持父子关系的一致性