tree-processor 0.9.1 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +1307 -1307
  3. package/README.md +1307 -1307
  4. package/dist/stats.html +1 -1
  5. package/package.json +78 -78
package/README.md CHANGED
@@ -1,1307 +1,1307 @@
1
- # tree-processor
2
-
3
- <div align="right">
4
-
5
- [English](https://github.com/knott11/tree-processor/blob/main/README.en.md) | [中文](https://github.com/knott11/tree-processor/blob/main/README.md)
6
-
7
- </div>
8
-
9
- <div align="center">
10
-
11
- ![npm version](https://img.shields.io/npm/v/tree-processor?style=flat-square)
12
- ![npm downloads (2 months)](https://img.shields.io/badge/downloads-1.3K%2F2mo-brightgreen?style=flat-square)
13
- ![bundle size](https://img.shields.io/badge/bundle-8.4KB-blue?style=flat-square)
14
- ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
15
- ![coverage](https://img.shields.io/badge/coverage-99%25-brightgreen?style=flat-square)
16
-
17
- 一个轻量级的树结构数据处理工具库,使用 TypeScript 编写,支持 tree-shaking,每个格式打包体积约 **8.2-8.5 KB**(ESM: 8.24 KB,CJS: 8.51 KB,UMD: 8.52 KB)。
18
-
19
-
20
- </div>
21
-
22
- ## 📋 目录
23
-
24
- - [特性](#-特性)
25
- - [使用场景](#-使用场景)
26
- - [安装](#-安装)
27
- - [快速开始](#-快速开始)
28
- - [API 文档](#-api-文档)
29
- - [遍历方法](#遍历方法)
30
- - [查找方法](#查找方法)
31
- - [访问方法](#访问方法)
32
- - [修改方法](#修改方法)
33
- - [转换方法](#转换方法)
34
- - [查询方法](#查询方法)
35
- - [验证方法](#验证方法)
36
- - [自定义字段名](#自定义字段名)
37
- - [测试](#测试)
38
- - [开发](#开发)
39
-
40
- ## ✨ 特性
41
-
42
- - **轻量级** - 每个格式打包体积仅 8.2-8.5 KB(ESM: 8.24 KB,CJS: 8.51 KB,UMD: 8.52 KB),对项目体积影响极小
43
- - **支持 Tree-shaking** - 支持按需导入,只打包实际使用的代码,进一步减小打包体积
44
- - **完整的 TypeScript 支持** - 提供完整的类型定义和智能提示,提升开发体验
45
- - **灵活的自定义字段名** - 支持自定义 children 和 id 字段名,适配各种数据结构
46
- - **零依赖** - 无任何外部依赖,开箱即用,无需担心依赖冲突
47
- - **完善的测试覆盖** - 包含 328 个测试用例,测试覆盖率达到 99%+(语句覆盖率 99%,分支覆盖率 98.41%,函数覆盖率 100%,行覆盖率 98.99%),覆盖基础功能、边界情况、异常处理、复杂场景等
48
- - **丰富的 API** - 提供 32+ 个方法,包含类似数组的 API(map、filter、find、some、every等),以及树结构特有的操作(获取父子节点、深度计算、数据验证、格式转换等),涵盖遍历、查找、修改、转换、判断等完整场景
49
-
50
- **已支持的方法:** mapTree、forEachTree、filterTree、findTree、pushTree、unshiftTree、popTree、shiftTree、someTree、everyTree、includesTree、atTree、indexOfTree、atIndexOfTree、dedupTree、removeTree、getParentTree、getChildrenTree、getSiblingsTree、getNodeDepthMap、getNodeDepth、isLeafNode、isRootNode、isEmptyTreeData、isEmptySingleTreeData、isTreeData、isSingleTreeData、isValidTreeNode、isTreeNodeWithCircularCheck、isSafeTreeDepth、convertToArrayTree、convertBackTree、convertToMapTree、convertToLevelArrayTree、convertToObjectTree。每个方法的最后一个参数可以自定义 children 和 id 的属性名。
51
-
52
- ### 💡 使用场景
53
-
54
- - **导航系统** - 多级菜单、路由配置的展开、折叠、搜索、过滤
55
- - **文件系统** - 文件目录的遍历、查找、移动、删除
56
- - **权限系统** - 组织架构、角色权限的树形结构管理和验证
57
- - **框架开发** - 组件树、路由树等树形结构的构建和管理
58
- - **数据管理** - 分类管理、评论系统、树形选择器等数据操作
59
-
60
- ## 📦 安装
61
-
62
- ```bash
63
- npm install tree-processor
64
- # 或
65
- yarn add tree-processor
66
- # 或
67
- pnpm add tree-processor
68
- ```
69
-
70
- ## 🚀 快速开始
71
-
72
- ```javascript
73
- import { mapTree, findTree, filterTree } from 'tree-processor'
74
-
75
- const treeData = [
76
- {
77
- id: 1,
78
- name: 'node1',
79
- children: [
80
- { id: 2, name: 'node2' },
81
- { id: 3, name: 'node3' },
82
- ],
83
- },
84
- ]
85
-
86
- // 获取所有节点名称
87
- const names = mapTree(treeData, (node) => node.name)
88
- console.log(names) // ['node1', 'node2', 'node3']
89
-
90
- // 查找节点
91
- const node = findTree(treeData, (n) => n.id === 2)
92
- console.log(node) // { id: 2, name: 'node2' }
93
-
94
- // 过滤节点
95
- const filtered = filterTree(treeData, (n) => n.id > 1)
96
- console.log(filtered) // [{ id: 2, name: 'node2' }, { id: 3, name: 'node3' }]
97
- ```
98
-
99
- ## 📖 API 文档
100
-
101
- ### 引入方式
102
-
103
- #### 默认导入(推荐用于需要多个方法的场景)
104
-
105
- ```javascript
106
- // ES Module
107
- import t from 'tree-processor'
108
-
109
- // CommonJS
110
- const t = require('tree-processor')
111
- ```
112
-
113
- #### 按需导入(推荐用于只需要少量方法的场景,支持 tree-shaking)
114
-
115
- ```javascript
116
- // ES Module - 按需导入单个方法
117
- import { mapTree, filterTree, findTree } from 'tree-processor'
118
-
119
- // ES Module - 按需导入类型
120
- import type { TreeNode, TreeData, FieldNames } from 'tree-processor'
121
-
122
- // CommonJS - 按需导入
123
- const { mapTree, filterTree } = require('tree-processor')
124
- ```
125
-
126
- ### 示例数据
127
-
128
- 以下示例数据将用于后续所有方法的演示:
129
-
130
- ```javascript
131
- const treeData = [
132
- {
133
- id: 1,
134
- name: 'node1',
135
- children: [
136
- {
137
- id: 2,
138
- name: 'node2',
139
- children: [
140
- { id: 4, name: 'node4' },
141
- { id: 5, name: 'node5' },
142
- ],
143
- },
144
- {
145
- id: 3,
146
- name: 'node3',
147
- children: [{ id: 6, name: 'node6' }],
148
- },
149
- ],
150
- },
151
- ];
152
- ```
153
-
154
- ---
155
-
156
- ## 遍历方法
157
-
158
- ### mapTree
159
-
160
- 遍历树结构数据,对每个节点执行回调函数,返回映射后的数组。
161
-
162
- ```javascript
163
- // 获取所有节点的名称
164
- const nodeNames = t.mapTree(treeData, (node) => node.name)
165
- console.log(nodeNames) // ['node1', 'node2', 'node4', 'node5', 'node3', 'node6']
166
-
167
- // 获取所有节点的ID
168
- const nodeIds = t.mapTree(treeData, (node) => node.id)
169
- console.log(nodeIds) // [1, 2, 4, 5, 3, 6]
170
-
171
- // 修改节点数据
172
- const modifiedNodes = t.mapTree(treeData, (node) => ({
173
- ...node,
174
- label: node.name
175
- }))
176
- console.log(modifiedNodes) // 返回包含 label 字段的新数组
177
- ```
178
-
179
- ### forEachTree
180
-
181
- 遍历树结构数据,对每个节点执行回调函数。与 mapTree 的区别是不返回值,性能更好,适合只需要遍历而不需要返回结果的场景。
182
-
183
- ```javascript
184
- // 遍历所有节点并打印
185
- t.forEachTree(treeData, (node) => {
186
- console.log(node)
187
- })
188
-
189
- // 修改节点属性
190
- t.forEachTree(treeData, (node) => {
191
- node.visited = true
192
- node.timestamp = Date.now()
193
- })
194
-
195
- // 统计节点数量
196
- let nodeCount = 0
197
- t.forEachTree(treeData, () => {
198
- nodeCount++
199
- })
200
- console.log(nodeCount) // 节点总数
201
- ```
202
-
203
- ---
204
-
205
- ## 查找方法
206
-
207
- ### filterTree
208
-
209
- 过滤树结构数据,返回满足条件的节点。
210
-
211
- ```javascript
212
- // 过滤出名称包含 'node' 的节点
213
- const filteredNodes = t.filterTree(treeData, (node) => {
214
- return node.name.includes('node')
215
- })
216
- console.log(filteredNodes) // 返回满足条件的节点数组
217
-
218
- // 过滤出ID大于2的节点
219
- const nodesWithLargeId = t.filterTree(treeData, (node) => node.id > 2)
220
- console.log(nodesWithLargeId) // 返回ID大于2的节点数组
221
-
222
- // 过滤出没有子节点的节点(叶子节点)
223
- const leafNodes = t.filterTree(treeData, (node) => {
224
- return !node.children || node.children.length === 0
225
- })
226
- console.log(leafNodes) // 返回所有叶子节点
227
- ```
228
-
229
- ### findTree
230
-
231
- 查找树结构数据中满足条件的第一个节点。如果未找到,返回 null。
232
-
233
- ```javascript
234
- // 查找ID为2的节点
235
- const foundNode = t.findTree(treeData, (node) => node.id === 2)
236
- console.log(foundNode) // 返回找到的节点对象,未找到返回 null
237
-
238
- // 查找名称为 'node3' 的节点
239
- const node3 = t.findTree(treeData, (node) => node.name === 'node3')
240
- console.log(node3) // { id: 3, name: 'node3', children: [...] }
241
-
242
- // 查找不存在的节点
243
- const nodeNotFound = t.findTree(treeData, (node) => node.id === 999)
244
- console.log(nodeNotFound) // null
245
- ```
246
-
247
- ### includesTree
248
-
249
- 检查树结构数据中是否包含指定ID的节点。
250
-
251
- ```javascript
252
- const nodeId = 2
253
- const hasNode = t.includesTree(treeData, nodeId)
254
-
255
- console.log(hasNode) // true 表示包含该节点,false 表示不包含
256
- ```
257
-
258
- ### someTree
259
-
260
- 检查树结构数据中是否存在满足条件的节点。只要有一个节点满足条件就返回 true。
261
-
262
- ```javascript
263
- // 检查是否存在名称为 'node2' 的节点
264
- const hasNode2 = t.someTree(treeData, node => node.name === 'node2')
265
- console.log(hasNode2) // true
266
-
267
- // 检查是否存在ID大于10的节点
268
- const hasLargeId = t.someTree(treeData, node => node.id > 10)
269
- console.log(hasLargeId) // false
270
- ```
271
-
272
- ### everyTree
273
-
274
- 检查树结构数据中是否所有节点都满足条件。只有所有节点都满足条件才返回 true。
275
-
276
- ```javascript
277
- // 检查所有节点的ID是否都大于0
278
- const allIdsPositive = t.everyTree(treeData, node => node.id > 0)
279
- console.log(allIdsPositive) // true
280
-
281
- // 检查所有节点是否都有 name 属性
282
- const allHaveName = t.everyTree(treeData, node => node.name)
283
- console.log(allHaveName) // 根据实际数据返回 true 或 false
284
- ```
285
-
286
- ---
287
-
288
- ## 访问方法
289
-
290
- ### atTree
291
-
292
- 根据父节点ID和子节点索引获取节点。支持负数索引,和数组的 at 方法一样。未找到返回 null。
293
-
294
- ```javascript
295
- // 获取ID为1的节点的第一个子节点(索引0)
296
- const firstChildNode = t.atTree(treeData, 1, 0)
297
- console.log(firstChildNode) // 返回第一个子节点
298
-
299
- // 获取最后一个子节点(负数索引)
300
- const lastChildNode = t.atTree(treeData, 1, -1)
301
- console.log(lastChildNode) // 返回最后一个子节点
302
-
303
- // 索引超出范围返回 null
304
- const nodeNotFound = t.atTree(treeData, 1, 10)
305
- console.log(nodeNotFound) // null
306
- ```
307
-
308
- ### indexOfTree
309
-
310
- 返回一个数组,值为从根节点开始到 targetId 所在节点的索引路径。未找到返回 null。返回值可以传入 atIndexOfTree 的第二个参数进行取值。
311
-
312
- ```javascript
313
- // 获取ID为4的节点的索引路径
314
- const nodePath = t.indexOfTree(treeData, 4)
315
- console.log(nodePath) // [0, 0, 0] 表示根节点 -> 第一个子节点 -> 第一个子节点
316
-
317
- // 未找到节点返回 null
318
- const pathNotFound = t.indexOfTree(treeData, 999)
319
- console.log(pathNotFound) // null
320
-
321
- // 结合 atIndexOfTree 使用
322
- const indexPath = t.indexOfTree(treeData, 4)
323
- const nodeByPath = t.atIndexOfTree(treeData, indexPath)
324
- console.log(nodeByPath) // 获取到ID为4的节点
325
- ```
326
-
327
- ### atIndexOfTree
328
-
329
- 根据索引路径获取节点。路径无效或超出范围返回 null。
330
-
331
- ```javascript
332
- // 根据索引路径获取节点
333
- const nodeByIndexPath = t.atIndexOfTree(treeData, [0, 1, 0])
334
- console.log(nodeByIndexPath) // 返回对应路径的节点对象
335
-
336
- // 结合 indexOfTree 使用
337
- const targetPath = t.indexOfTree(treeData, 4)
338
- const targetNode = t.atIndexOfTree(treeData, targetPath)
339
- console.log(targetNode) // 获取到ID为4的节点
340
-
341
- // 路径无效返回 null
342
- const invalidPath = t.atIndexOfTree(treeData, [999])
343
- console.log(invalidPath) // null
344
- ```
345
-
346
- ---
347
-
348
- ## 修改方法
349
-
350
- ### pushTree
351
-
352
- 在指定节点下添加子节点到末尾。返回 true 表示添加成功,false 表示未找到目标节点。
353
-
354
- ```javascript
355
- // 在ID为1的节点下添加新子节点
356
- const addSuccess = t.pushTree(treeData, 1, { id: 7, name: 'node7' })
357
- console.log(addSuccess) // true
358
- console.log(treeData) // 新节点已添加到 children 数组末尾
359
-
360
- // 尝试在不存在的节点下添加
361
- const addFailed = t.pushTree(treeData, 999, { id: 8, name: 'node8' })
362
- console.log(addFailed) // false,未找到目标节点
363
- ```
364
-
365
- ### unshiftTree
366
-
367
- 在指定节点下添加子节点到开头。返回 true 表示添加成功,false 表示未找到目标节点。
368
-
369
- ```javascript
370
- // 在ID为1的节点下添加新子节点到开头
371
- const unshiftSuccess = t.unshiftTree(treeData, 1, { id: 7, name: 'node7' })
372
- console.log(unshiftSuccess) // true
373
- console.log(treeData) // 新节点已添加到 children 数组开头
374
- ```
375
-
376
- ### popTree
377
-
378
- 删除指定节点下的最后一个子节点。返回被删除的节点,如果节点不存在或没有子节点则返回 false。
379
-
380
- ```javascript
381
- // 删除ID为1的节点下的最后一个子节点
382
- const removedNode = t.popTree(treeData, 1)
383
- console.log(removedNode) // 返回被删除的节点对象,或 false
384
-
385
- // 尝试删除不存在的节点下的子节点
386
- const popFailed = t.popTree(treeData, 999)
387
- console.log(popFailed) // false
388
- ```
389
-
390
- ### shiftTree
391
-
392
- 删除指定节点下的第一个子节点。返回被删除的节点,如果节点不存在或没有子节点则返回 false。
393
-
394
- ```javascript
395
- // 删除ID为1的节点下的第一个子节点
396
- const shiftedNode = t.shiftTree(treeData, 1)
397
- console.log(shiftedNode) // 返回被删除的节点对象,或 false
398
- ```
399
-
400
- ### removeTree
401
-
402
- 删除树结构数据中指定ID的节点,包括根节点和子节点。
403
-
404
- ```javascript
405
- const nodeIdToRemove = 2
406
- const removeSuccess = t.removeTree(treeData, nodeIdToRemove)
407
-
408
- console.log(removeSuccess) // true 表示删除成功,false 表示未找到节点
409
- console.log(treeData) // 删除后的树结构
410
- ```
411
-
412
- ### dedupTree
413
-
414
- 树结构对象数组去重方法,根据指定的键去除重复节点。保留第一次出现的节点。
415
-
416
- ```javascript
417
- // 根据 id 字段去重
418
- const uniqueTreeData = t.dedupTree(treeData, 'id')
419
- console.log(uniqueTreeData) // 返回去重后的树结构数据
420
-
421
- // 根据 name 字段去重
422
- const uniqueByNameTree = t.dedupTree(treeData, 'name')
423
- console.log(uniqueByNameTree) // 返回根据 name 去重后的数据
424
- ```
425
-
426
- ---
427
-
428
- ## 转换方法
429
-
430
- ### convertToArrayTree
431
-
432
- 将树结构数据扁平化为数组。返回的数组中每个节点都不包含 `children` 字段。
433
-
434
- ```javascript
435
- // 将树结构扁平化为数组
436
- const array = t.convertToArrayTree(treeData)
437
- console.log(array)
438
- // [
439
- // { id: 1, name: 'node1' },
440
- // { id: 2, name: 'node2' },
441
- // { id: 4, name: 'node4' },
442
- // { id: 5, name: 'node5' },
443
- // { id: 3, name: 'node3' },
444
- // { id: 6, name: 'node6' }
445
- // ]
446
-
447
- // 注意:返回的节点不包含 children 字段
448
- array.forEach(node => {
449
- console.log(node.children) // undefined
450
- })
451
-
452
- // 支持自定义字段名
453
- const customTree = [
454
- {
455
- nodeId: 1,
456
- name: 'node1',
457
- subNodes: [
458
- { nodeId: 2, name: 'node2' }
459
- ]
460
- }
461
- ]
462
- const customArray = t.convertToArrayTree(customTree, {
463
- children: 'subNodes',
464
- id: 'nodeId'
465
- })
466
- console.log(customArray) // 扁平化后的数组,不包含 subNodes 字段
467
- ```
468
-
469
- ### convertToMapTree
470
-
471
- 将树结构数据转换为 Map,key 为节点 ID,value 为节点对象(不包含 children 字段)。适用于需要快速通过 ID 查找节点的场景。
472
-
473
- ```javascript
474
- // 将树结构转换为 Map
475
- const map = t.convertToMapTree(treeData)
476
- console.log(map instanceof Map) // true
477
- console.log(map.size) // 6
478
-
479
- // 通过 ID 快速查找节点
480
- const node = map.get(2)
481
- console.log(node) // { id: 2, name: 'node2' }
482
- console.log(node.children) // undefined(不包含 children 字段)
483
-
484
- // 支持自定义字段名
485
- const customTree = [
486
- {
487
- nodeId: 1,
488
- name: 'node1',
489
- subNodes: [
490
- { nodeId: 2, name: 'node2' }
491
- ]
492
- }
493
- ]
494
- const customMap = t.convertToMapTree(customTree, {
495
- children: 'subNodes',
496
- id: 'nodeId'
497
- })
498
- console.log(customMap.get(1)) // { nodeId: 1, name: 'node1' }
499
- ```
500
-
501
- ### convertToLevelArrayTree
502
-
503
- 将树结构数据转换为层级数组(二维数组),按深度分组。外层数组按深度索引,内层数组包含该深度的所有节点。
504
-
505
- ```javascript
506
- // 将树结构转换为层级数组
507
- const levelArray = t.convertToLevelArrayTree(treeData)
508
- console.log(levelArray)
509
- // [
510
- // [{ id: 1, name: 'node1' }], // 第 0 层
511
- // [{ id: 2, name: 'node2' }, { id: 3, name: 'node3' }], // 第 1 层
512
- // [{ id: 4, name: 'node4' }, { id: 5, name: 'node5' }, { id: 6, name: 'node6' }] // 第 2 层
513
- // ]
514
-
515
- // 遍历每一层
516
- levelArray.forEach((level, depth) => {
517
- console.log(`深度 ${depth}:`, level)
518
- })
519
-
520
- // 注意:返回的节点不包含 children 字段
521
- levelArray[0][0].children // undefined
522
-
523
- // 支持自定义字段名
524
- const customTree = [
525
- {
526
- nodeId: 1,
527
- name: 'node1',
528
- subNodes: [
529
- { nodeId: 2, name: 'node2' }
530
- ]
531
- }
532
- ]
533
- const customLevelArray = t.convertToLevelArrayTree(customTree, {
534
- children: 'subNodes',
535
- id: 'nodeId'
536
- })
537
- console.log(customLevelArray) // 按层级分组的数组
538
- ```
539
-
540
- ### convertToObjectTree
541
-
542
- 将单根树结构数据转换为对象。如果树只有一个根节点,返回该节点对象;否则返回 `null`。
543
-
544
- ```javascript
545
- // 单根树转换为对象
546
- const singleRootTree = [
547
- {
548
- id: 1,
549
- name: 'node1',
550
- value: 100,
551
- children: [
552
- { id: 2, name: 'node2' }
553
- ]
554
- }
555
- ]
556
- const rootNode = t.convertToObjectTree(singleRootTree)
557
- console.log(rootNode)
558
- // {
559
- // id: 1,
560
- // name: 'node1',
561
- // value: 100,
562
- // children: [{ id: 2, name: 'node2' }]
563
- // }
564
-
565
- // 多个根节点返回 null
566
- const multiRootTree = [
567
- { id: 1, name: 'node1' },
568
- { id: 2, name: 'node2' }
569
- ]
570
- const result = t.convertToObjectTree(multiRootTree)
571
- console.log(result) // null
572
-
573
- // 空树返回 null
574
- const emptyTree = []
575
- const emptyResult = t.convertToObjectTree(emptyTree)
576
- console.log(emptyResult) // null
577
- ```
578
-
579
- ### convertBackTree
580
-
581
- 将各种数据结构转换为树结构数据。支持数组、Map、Record(对象)等格式。数组中的每个元素需要包含 `id` 和 `parentId` 字段。
582
-
583
- ```javascript
584
- // 将扁平数组转换为树结构
585
- const array = [
586
- { id: 1, name: 'node1', parentId: null },
587
- { id: 2, name: 'node2', parentId: 1 },
588
- { id: 3, name: 'node3', parentId: 1 },
589
- { id: 4, name: 'node4', parentId: 2 },
590
- { id: 5, name: 'node5', parentId: 2 },
591
- { id: 6, name: 'node6', parentId: 3 }
592
- ]
593
- const tree = t.convertBackTree(array)
594
- console.log(tree)
595
- // [
596
- // {
597
- // id: 1,
598
- // name: 'node1',
599
- // children: [
600
- // {
601
- // id: 2,
602
- // name: 'node2',
603
- // children: [
604
- // { id: 4, name: 'node4', children: [] },
605
- // { id: 5, name: 'node5', children: [] }
606
- // ]
607
- // },
608
- // {
609
- // id: 3,
610
- // name: 'node3',
611
- // children: [
612
- // { id: 6, name: 'node6', children: [] }
613
- // ]
614
- // }
615
- // ]
616
- // }
617
- // ]
618
-
619
- // 自定义根节点的 parentId 值
620
- const arrayWithZero = [
621
- { id: 1, name: 'node1', parentId: 0 },
622
- { id: 2, name: 'node2', parentId: 1 }
623
- ]
624
- const treeWithZero = t.convertBackTree(arrayWithZero, { rootParentId: 0 })
625
- console.log(treeWithZero) // 正确转换
626
-
627
- // 自定义 parentId 字段名
628
- const arrayWithPid = [
629
- { id: 1, name: 'node1', pid: null },
630
- { id: 2, name: 'node2', pid: 1 }
631
- ]
632
- const treeWithPid = t.convertBackTree(arrayWithPid, { parentIdField: 'pid' })
633
- console.log(treeWithPid) // 正确转换
634
-
635
- // 支持自定义字段名
636
- const customArray = [
637
- { nodeId: 1, name: 'node1', parentId: null },
638
- { nodeId: 2, name: 'node2', parentId: 1 }
639
- ]
640
- const customTree = t.convertBackTree(customArray, {
641
- fieldNames: { id: 'nodeId', children: 'subNodes' }
642
- })
643
- console.log(customTree)
644
- // [
645
- // {
646
- // nodeId: 1,
647
- // name: 'node1',
648
- // subNodes: [
649
- // { nodeId: 2, name: 'node2', subNodes: [] }
650
- // ]
651
- // }
652
- // ]
653
-
654
- // 处理多个根节点
655
- const multiRootArray = [
656
- { id: 1, name: 'root1', parentId: null },
657
- { id: 2, name: 'root2', parentId: null },
658
- { id: 3, name: 'child1', parentId: 1 }
659
- ]
660
- const multiRootTree = t.convertBackTree(multiRootArray)
661
- console.log(multiRootTree) // 包含两个根节点
662
- ```
663
-
664
- **参数说明:**
665
- - `data` - 支持多种数据格式:
666
- - 数组:扁平数组,每个元素需要包含 `id` 和 `parentId` 字段
667
- - Map:key 为节点 ID,value 为节点对象
668
- - Record(对象):key 为节点 ID,value 为节点对象
669
- - 单个对象:单个树节点对象
670
- - `options.rootParentId` - 根节点的 parentId 值,默认为 `null`
671
- - `options.parentIdField` - 父节点ID字段名,默认为 `'parentId'`
672
- - `options.fieldNames` - 自定义字段名配置,支持自定义 `id` 和 `children` 字段名
673
-
674
- **注意事项:**
675
- - 如果节点的 `parentId` 找不到对应的父节点,该节点会被作为根节点处理
676
- - 没有 `id` 的节点会被跳过
677
- - `parentId` 为 `null`、`undefined` 或等于 `rootParentId` 的节点会被视为根节点
678
- - Map 和 Record 格式转换时,key 会被设置为节点的 `id`
679
-
680
- **示例:支持 Map 和 Record 格式**
681
-
682
- ```javascript
683
- // Map 格式
684
- const map = new Map([
685
- [1, { name: 'node1', parentId: null }],
686
- [2, { name: 'node2', parentId: 1 }]
687
- ])
688
- const treeFromMap = t.convertBackTree(map)
689
- console.log(treeFromMap) // 正确转换为树结构
690
-
691
- // Record 格式
692
- const record = {
693
- 1: { name: 'node1', parentId: null },
694
- 2: { name: 'node2', parentId: 1 }
695
- }
696
- const treeFromRecord = t.convertBackTree(record)
697
- console.log(treeFromRecord) // 正确转换为树结构
698
- ```
699
-
700
- ---
701
-
702
- ## 查询方法
703
-
704
- ### getParentTree
705
-
706
- 获取指定节点的父节点。如果节点是根节点或未找到,返回 null。
707
-
708
- ```javascript
709
- // 获取ID为2的节点的父节点
710
- const parentNode = t.getParentTree(treeData, 2)
711
- console.log(parentNode) // 返回父节点对象 { id: 1, name: 'node1', ... }
712
-
713
- // 根节点没有父节点,返回 null
714
- const rootParentNode = t.getParentTree(treeData, 1)
715
- console.log(rootParentNode) // null
716
-
717
- // 未找到节点返回 null
718
- const parentNotFound = t.getParentTree(treeData, 999)
719
- console.log(parentNotFound) // null
720
- ```
721
-
722
- ### getChildrenTree
723
-
724
- 获取指定节点的所有直接子节点。如果未找到节点或没有子节点,返回空数组。
725
-
726
- ```javascript
727
- // 获取ID为1的节点的所有子节点
728
- const children = t.getChildrenTree(treeData, 1)
729
- console.log(children) // 返回子节点数组 [{ id: 2, ... }, { id: 3, ... }]
730
-
731
- // 节点没有子节点,返回空数组
732
- const emptyChildren = t.getChildrenTree(treeData, 4)
733
- console.log(emptyChildren) // []
734
-
735
- // 未找到节点返回空数组
736
- const notFound = t.getChildrenTree(treeData, 999)
737
- console.log(notFound) // []
738
-
739
- // 支持自定义字段名
740
- const customTree = [
741
- {
742
- nodeId: 1,
743
- name: 'root',
744
- subNodes: [
745
- { nodeId: 2, name: 'child1' },
746
- { nodeId: 3, name: 'child2' },
747
- ],
748
- },
749
- ];
750
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
751
- const customChildren = t.getChildrenTree(customTree, 1, fieldNames)
752
- console.log(customChildren) // 返回子节点数组
753
- ```
754
-
755
- ### getSiblingsTree
756
-
757
- 获取指定节点的所有兄弟节点(包括自己)。如果未找到节点,返回空数组。根节点的兄弟节点是其他根节点。
758
-
759
- ```javascript
760
- // 获取ID为2的节点的所有兄弟节点(包括自己)
761
- const siblings = t.getSiblingsTree(treeData, 2)
762
- console.log(siblings) // 返回兄弟节点数组 [{ id: 2, ... }, { id: 3, ... }]
763
-
764
- // 根节点的兄弟节点是其他根节点
765
- const multiRoot = [
766
- { id: 1, children: [{ id: 2 }] },
767
- { id: 3, children: [{ id: 4 }] },
768
- ];
769
- const rootSiblings = t.getSiblingsTree(multiRoot, 1)
770
- console.log(rootSiblings) // 返回所有根节点 [{ id: 1, ... }, { id: 3, ... }]
771
-
772
- // 未找到节点返回空数组
773
- const notFound = t.getSiblingsTree(treeData, 999)
774
- console.log(notFound) // []
775
-
776
- // 支持自定义字段名
777
- const customTree = [
778
- {
779
- nodeId: 1,
780
- name: 'root',
781
- subNodes: [
782
- { nodeId: 2, name: 'child1' },
783
- { nodeId: 3, name: 'child2' },
784
- { nodeId: 4, name: 'child3' },
785
- ],
786
- },
787
- ];
788
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
789
- const customSiblings = t.getSiblingsTree(customTree, 2, fieldNames)
790
- console.log(customSiblings) // 返回兄弟节点数组(包括自己)
791
- ```
792
-
793
- ### getNodeDepthMap
794
-
795
- 返回一个字典,键代表节点的 id,值代表该节点在数据的第几层。深度从1开始,根节点深度为1。
796
-
797
- ```javascript
798
- // 获取所有节点的深度映射
799
- const nodeDepthMap = t.getNodeDepthMap(treeData)
800
- console.log(nodeDepthMap) // { 1: 1, 2: 2, 3: 2, 4: 3, 5: 3, 6: 3 }
801
-
802
- // 获取特定节点的深度
803
- const node2Depth = nodeDepthMap[2]
804
- console.log(node2Depth) // 2
805
-
806
- // 空树返回空对象
807
- const emptyDepthMap = t.getNodeDepthMap([])
808
- console.log(emptyDepthMap) // {}
809
- ```
810
-
811
- ### getNodeDepth
812
-
813
- 获取指定节点的深度。深度从1开始,根节点深度为1。
814
-
815
- ```javascript
816
- // 获取根节点的深度
817
- const rootDepth = t.getNodeDepth(treeData, 1)
818
- console.log(rootDepth) // 1
819
-
820
- // 获取子节点的深度
821
- const childDepth = t.getNodeDepth(treeData, 2)
822
- console.log(childDepth) // 2
823
-
824
- // 获取深层节点的深度
825
- const deepDepth = t.getNodeDepth(treeData, 4)
826
- console.log(deepDepth) // 3
827
-
828
- // 未找到节点返回 null
829
- const notFound = t.getNodeDepth(treeData, 999)
830
- console.log(notFound) // null
831
-
832
- // 支持自定义字段名
833
- const customTree = [
834
- {
835
- nodeId: 1,
836
- name: 'root',
837
- subNodes: [
838
- { nodeId: 2, name: 'child' },
839
- ],
840
- },
841
- ];
842
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
843
- const depth = t.getNodeDepth(customTree, 2, fieldNames)
844
- console.log(depth) // 2
845
- ```
846
-
847
- **与 getNodeDepthMap 的区别:**
848
- - `getNodeDepthMap` - 批量获取所有节点的深度(一次性计算所有节点)
849
- - `getNodeDepth` - 只获取单个节点的深度(只计算目标节点,效率更高)
850
-
851
- ---
852
-
853
- ## 验证方法
854
-
855
- ### isLeafNode
856
-
857
- 检查节点是否是叶子节点(没有子节点)。轻量级方法,只检查节点本身,不遍历树。
858
-
859
- ```javascript
860
- // 没有 children 字段的节点是叶子节点
861
- const leafNode1 = { id: 1, name: 'node1' };
862
- console.log(t.isLeafNode(leafNode1)) // true
863
-
864
- // children 为空数组的节点是叶子节点
865
- const leafNode2 = { id: 2, name: 'node2', children: [] };
866
- console.log(t.isLeafNode(leafNode2)) // true
867
-
868
- // 有子节点的节点不是叶子节点
869
- const parentNode = {
870
- id: 3,
871
- name: 'node3',
872
- children: [{ id: 4, name: 'node4' }],
873
- };
874
- console.log(t.isLeafNode(parentNode)) // false
875
-
876
- // 在 filterTree 中使用(过滤出所有叶子节点)
877
- const leafNodes = t.filterTree(treeData, (node) => t.isLeafNode(node))
878
- console.log(leafNodes) // 返回所有叶子节点
879
-
880
- // 在 forEachTree 中使用
881
- t.forEachTree(treeData, (node) => {
882
- if (t.isLeafNode(node)) {
883
- console.log('叶子节点:', node.name)
884
- }
885
- })
886
-
887
- // 支持自定义字段名
888
- const customNode = {
889
- nodeId: 1,
890
- name: 'node1',
891
- subNodes: [],
892
- };
893
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
894
- console.log(t.isLeafNode(customNode, fieldNames)) // true
895
- ```
896
-
897
- **与现有方法的区别:**
898
- - `isLeafNode` - 只检查单个节点,轻量级(O(1)),适合在遍历时使用
899
- - `getChildrenTree` - 获取子节点数组,需要传入 tree 和 nodeId,需要查找节点(O(n))
900
-
901
- ### isRootNode
902
-
903
- 检查节点是否是根节点(没有父节点)。根节点是树结构数据数组中的顶层节点。
904
-
905
- ```javascript
906
- // 检查根节点
907
- const treeData = [
908
- {
909
- id: 1,
910
- name: 'root1',
911
- children: [{ id: 2, name: 'child1' }],
912
- },
913
- ];
914
- console.log(t.isRootNode(treeData, 1)) // true
915
- console.log(t.isRootNode(treeData, 2)) // false
916
-
917
- // 多个根节点的情况
918
- const multiRoot = [
919
- { id: 1, name: 'root1' },
920
- { id: 2, name: 'root2' },
921
- { id: 3, name: 'root3' },
922
- ];
923
- console.log(t.isRootNode(multiRoot, 1)) // true
924
- console.log(t.isRootNode(multiRoot, 2)) // true
925
- console.log(t.isRootNode(multiRoot, 3)) // true
926
-
927
- // 在遍历时使用
928
- t.forEachTree(treeData, (node) => {
929
- if (t.isRootNode(treeData, node.id)) {
930
- console.log('根节点:', node.name)
931
- }
932
- })
933
-
934
- // 支持自定义字段名
935
- const customTree = [
936
- {
937
- nodeId: 1,
938
- name: 'root1',
939
- subNodes: [{ nodeId: 2, name: 'child1' }],
940
- },
941
- ];
942
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
943
- console.log(t.isRootNode(customTree, 1, fieldNames)) // true
944
- console.log(t.isRootNode(customTree, 2, fieldNames)) // false
945
-
946
- // 节点不存在时返回 false
947
- console.log(t.isRootNode(treeData, 999)) // false
948
- ```
949
-
950
- **与现有方法的区别:**
951
- - `isRootNode` - 语义化方法,直接返回布尔值
952
- - `getParentTree` - 返回父节点对象,需要判断是否为 null
953
- - `getNodeDepth` - 返回深度,需要判断是否等于 1
954
-
955
- ### isEmptyTreeData
956
-
957
- 检查树结构数据(数组)是否为空。空数组、null、undefined 都视为空。此函数支持 fieldNames 参数以保持 API 一致性,但该参数不生效(因为只检查数组是否为空,不访问 children 或 id 字段)。
958
-
959
- ```javascript
960
- // 检查树结构数据是否为空
961
- const isEmptyTree = t.isEmptyTreeData(treeData)
962
- console.log(isEmptyTree) // false(有数据)
963
-
964
- // 空数组返回 true
965
- const isEmptyArray = t.isEmptyTreeData([])
966
- console.log(isEmptyArray) // true
967
-
968
- // null 或 undefined 返回 true
969
- const isNullTree = t.isEmptyTreeData(null)
970
- console.log(isNullTree) // true
971
-
972
- // 支持 fieldNames 参数(保持 API 一致性,但不生效)
973
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
974
- const isEmptyWithFieldNames = t.isEmptyTreeData(treeData, fieldNames)
975
- console.log(isEmptyWithFieldNames) // false(结果与不传 fieldNames 相同)
976
- ```
977
-
978
- ### isEmptySingleTreeData
979
-
980
- 检查单个树结构数据是否为空。如果数据不是有效的单个树结构数据、没有 children 字段,或者 children 是空数组,则视为空。如果有子节点(children 数组不为空),即使子节点本身是空的,树也不为空。
981
-
982
- ```javascript
983
- // 没有 children 字段,视为空
984
- const tree1 = { id: 1, name: 'node1' };
985
- const isEmpty1 = t.isEmptySingleTreeData(tree1)
986
- console.log(isEmpty1) // true
987
-
988
- // children 是空数组,视为空
989
- const tree2 = {
990
- id: 1,
991
- name: 'node1',
992
- children: [],
993
- };
994
- const isEmpty2 = t.isEmptySingleTreeData(tree2)
995
- console.log(isEmpty2) // true
996
-
997
- // 有子节点,不为空
998
- const tree3 = {
999
- id: 1,
1000
- name: 'node1',
1001
- children: [
1002
- { id: 2, name: 'node2' },
1003
- ],
1004
- };
1005
- const isEmpty3 = t.isEmptySingleTreeData(tree3)
1006
- console.log(isEmpty3) // false
1007
-
1008
- // 有子节点,即使子节点本身是空的,树也不为空
1009
- const tree4 = {
1010
- id: 1,
1011
- name: 'node1',
1012
- children: [
1013
- { id: 2, name: 'node2', children: [] },
1014
- { id: 3, name: 'node3' }, // 没有children字段
1015
- ],
1016
- };
1017
- const isEmpty4 = t.isEmptySingleTreeData(tree4)
1018
- console.log(isEmpty4) // false(因为有子节点,即使子节点是空的)
1019
-
1020
- // 支持自定义字段名
1021
- const customTree = {
1022
- nodeId: 1,
1023
- name: 'node1',
1024
- subNodes: [],
1025
- };
1026
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1027
- const isEmptyCustom = t.isEmptySingleTreeData(customTree, fieldNames)
1028
- console.log(isEmptyCustom) // true
1029
- ```
1030
-
1031
- ### isTreeData
1032
-
1033
- 判断数据是否是树结构数据(数组)。树结构数据必须是一个数组,数组中的每个元素都必须是有效的单个树结构数据。
1034
-
1035
- ```javascript
1036
- // 有效的树结构数据(森林)
1037
- const forest = [
1038
- {
1039
- id: 1,
1040
- name: 'node1',
1041
- children: [{ id: 2, name: 'node2' }],
1042
- },
1043
- {
1044
- id: 3,
1045
- name: 'node3',
1046
- children: [{ id: 4, name: 'node4' }],
1047
- },
1048
- ];
1049
- console.log(t.isTreeData(forest)) // true
1050
-
1051
- // 空数组也是有效的树结构数据(空森林)
1052
- console.log(t.isTreeData([])) // true
1053
-
1054
- // 单个对象不是树结构数据(应该用 isSingleTreeData)
1055
- console.log(t.isTreeData({ id: 1 })) // false
1056
-
1057
- // 数组包含非树结构元素,返回 false
1058
- const invalidForest = [
1059
- { id: 1, children: [{ id: 2 }] },
1060
- 'not a tree', // 无效元素
1061
- ];
1062
- console.log(t.isTreeData(invalidForest)) // false
1063
-
1064
- // null 或 undefined 不是有效的树结构数据
1065
- console.log(t.isTreeData(null)) // false
1066
- console.log(t.isTreeData(undefined)) // false
1067
-
1068
- // 支持自定义字段名
1069
- const customForest = [
1070
- {
1071
- nodeId: 1,
1072
- name: 'node1',
1073
- subNodes: [{ nodeId: 2, name: 'node2' }],
1074
- },
1075
- ];
1076
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1077
- console.log(t.isTreeData(customForest, fieldNames)) // true
1078
- ```
1079
-
1080
- ### isSingleTreeData
1081
-
1082
- 判断数据是否是单个树结构数据(单个对象)。树结构数据必须是一个对象(不能是数组、null、undefined 或基本类型),如果存在 children 字段,必须是数组类型,并且会递归检查所有子节点。
1083
-
1084
- ```javascript
1085
- // 有效的单个树结构数据
1086
- const tree = {
1087
- id: 1,
1088
- name: 'node1',
1089
- children: [
1090
- { id: 2, name: 'node2' },
1091
- { id: 3, name: 'node3' },
1092
- ],
1093
- };
1094
- const isValid = t.isSingleTreeData(tree)
1095
- console.log(isValid) // true
1096
-
1097
- // 没有 children 字段也是有效的(只有根节点)
1098
- const singleNode = { id: 1, name: 'node1' }
1099
- console.log(t.isSingleTreeData(singleNode)) // true
1100
-
1101
- // 数组不是单个树结构数据
1102
- console.log(t.isSingleTreeData([])) // false
1103
-
1104
- // null 或 undefined 不是有效的树结构数据
1105
- console.log(t.isSingleTreeData(null)) // false
1106
- console.log(t.isSingleTreeData(undefined)) // false
1107
-
1108
- // children 不能是 null
1109
- const invalidTree = { id: 1, children: null }
1110
- console.log(t.isSingleTreeData(invalidTree)) // false
1111
-
1112
- // 支持自定义字段名
1113
- const customTree = {
1114
- nodeId: 1,
1115
- name: 'node1',
1116
- subNodes: [{ nodeId: 2, name: 'node2' }],
1117
- };
1118
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1119
- console.log(t.isSingleTreeData(customTree, fieldNames)) // true
1120
- ```
1121
-
1122
- ### isValidTreeNode
1123
-
1124
- 检查单个节点是否是有效的树节点结构(轻量级,不递归检查子节点)。只检查节点本身的结构,不检查子节点。
1125
-
1126
- ```javascript
1127
- // 有效的树节点(有 children 数组)
1128
- const node1 = {
1129
- id: 1,
1130
- name: 'node1',
1131
- children: [{ id: 2 }],
1132
- };
1133
- console.log(t.isValidTreeNode(node1)) // true
1134
-
1135
- // 有效的树节点(没有 children 字段)
1136
- const node2 = { id: 1, name: 'node1' };
1137
- console.log(t.isValidTreeNode(node2)) // true
1138
-
1139
- // 无效的树节点(children 不是数组)
1140
- const invalidNode = {
1141
- id: 1,
1142
- children: 'not an array',
1143
- };
1144
- console.log(t.isValidTreeNode(invalidNode)) // false
1145
-
1146
- // 支持自定义字段名
1147
- const customNode = {
1148
- nodeId: 1,
1149
- subNodes: [{ nodeId: 2 }],
1150
- };
1151
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1152
- console.log(t.isValidTreeNode(customNode, fieldNames)) // true
1153
- ```
1154
-
1155
- **与 isSingleTreeData 的区别:**
1156
- - `isValidTreeNode` - 只检查单个节点的基本结构,不递归检查子节点(轻量级)
1157
- - `isSingleTreeData` - 递归检查整个树结构,确保所有子节点都是有效的树结构
1158
-
1159
- ### isTreeNodeWithCircularCheck
1160
-
1161
- 检查节点是否是有效的树节点结构,并检测循环引用。使用 WeakSet 跟踪已访问的节点,如果发现循环引用则返回 false。
1162
-
1163
- ```javascript
1164
- // 有效的树节点(无循环引用)
1165
- const validNode = {
1166
- id: 1,
1167
- children: [
1168
- { id: 2, children: [{ id: 3 }] },
1169
- ],
1170
- };
1171
- console.log(t.isTreeNodeWithCircularCheck(validNode)) // true
1172
-
1173
- // 检测循环引用
1174
- const node1 = { id: 1, children: [] };
1175
- const node2 = { id: 2, children: [] };
1176
- node1.children.push(node2);
1177
- node2.children.push(node1); // 循环引用
1178
- console.log(t.isTreeNodeWithCircularCheck(node1)) // false
1179
-
1180
- // 检测自引用
1181
- const selfRefNode = { id: 1, children: [] };
1182
- selfRefNode.children.push(selfRefNode); // 自引用
1183
- console.log(t.isTreeNodeWithCircularCheck(selfRefNode)) // false
1184
-
1185
- // 支持自定义字段名
1186
- const customNode = {
1187
- nodeId: 1,
1188
- subNodes: [{ nodeId: 2 }],
1189
- };
1190
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1191
- console.log(t.isTreeNodeWithCircularCheck(customNode, fieldNames)) // true
1192
- ```
1193
-
1194
- **使用场景:**
1195
- - 在接收用户输入或外部数据时,检查是否有循环引用
1196
- - 数据验证,防止无限递归
1197
- - 调试时检查数据结构是否正确
1198
-
1199
- ### isSafeTreeDepth
1200
-
1201
- 检查树结构数据的深度是否安全(防止递归爆栈)。如果树的深度超过 `maxDepth`,返回 false。
1202
-
1203
- ```javascript
1204
- // 深度安全的树
1205
- const safeTree = [
1206
- {
1207
- id: 1,
1208
- children: [
1209
- { id: 2, children: [{ id: 3 }] },
1210
- ],
1211
- },
1212
- ];
1213
- console.log(t.isSafeTreeDepth(safeTree, 10)) // true(深度为3,小于10)
1214
-
1215
- // 深度超过最大深度
1216
- const deepTree = [
1217
- {
1218
- id: 1,
1219
- children: [
1220
- { id: 2, children: [{ id: 3 }] },
1221
- ],
1222
- },
1223
- ];
1224
- console.log(t.isSafeTreeDepth(deepTree, 2)) // false(深度为3,超过2)
1225
-
1226
- // 空树总是安全的
1227
- console.log(t.isSafeTreeDepth([], 10)) // true
1228
-
1229
- // 单层树
1230
- const singleLayer = [{ id: 1 }, { id: 2 }];
1231
- console.log(t.isSafeTreeDepth(singleLayer, 1)) // true
1232
-
1233
- // 支持自定义字段名
1234
- const customTree = [
1235
- {
1236
- nodeId: 1,
1237
- subNodes: [
1238
- { nodeId: 2, subNodes: [{ nodeId: 3 }] },
1239
- ],
1240
- },
1241
- ];
1242
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1243
- console.log(t.isSafeTreeDepth(customTree, 3, fieldNames)) // true
1244
- console.log(t.isSafeTreeDepth(customTree, 2, fieldNames)) // false
1245
- ```
1246
-
1247
- **使用场景:**
1248
- - 在处理大型树之前,先检查深度是否安全
1249
- - 防止递归调用栈溢出
1250
- - 性能优化,避免处理过深的树结构
1251
-
1252
- ## 自定义字段名
1253
-
1254
- 所有方法都支持自定义 children 和 id 的属性名,通过最后一个参数传入配置对象:
1255
-
1256
- ```javascript
1257
- // 使用默认字段名
1258
- const foundNode1 = t.findTree(treeData, (node) => node.id === 2)
1259
-
1260
- // 使用自定义字段名
1261
- const fieldNames = { children: 'subNodes', id: 'nodeId' };
1262
- const foundNode2 = t.findTree(customTreeData, (node) => node.nodeId === 2, fieldNames);
1263
- ```
1264
-
1265
- **注意:** 所有 30 个函数都支持 `fieldNames` 参数,保持 API 一致性。即使某些函数(如 `isEmptyTreeData`)中该参数不生效,也可以传入以保持代码风格一致。
1266
-
1267
- ## 测试
1268
-
1269
- ### 运行测试
1270
-
1271
- ```bash
1272
- # 运行所有测试(自动打包后测试源码 + 打包文件,656 个测试用例)
1273
- npm test
1274
-
1275
- # 运行所有测试(单次,不监听文件变化)
1276
- npm test -- --run
1277
-
1278
- # 仅测试源代码(328 个测试用例)
1279
- npm run test:src
1280
-
1281
- # 仅测试打包后的文件(328 个测试用例,需要先运行 npm run build)
1282
- npm run test:dist
1283
-
1284
- # 运行测试并生成覆盖率报告
1285
- npm run test:coverage
1286
- ```
1287
-
1288
- ## 开发
1289
-
1290
- ```bash
1291
- # 安装依赖
1292
- npm install
1293
-
1294
- # 运行测试
1295
- npm test
1296
-
1297
- # 构建项目(先删除 dist 目录,然后重新打包)
1298
- npm run build
1299
- ```
1300
-
1301
- <div align="center">
1302
-
1303
- 如果这个项目对你有帮助,请给它一个 ⭐️
1304
-
1305
- Made with by [knott11]
1306
-
1307
- </div>
1
+ # tree-processor
2
+
3
+ <div align="right">
4
+
5
+ [English](https://github.com/knott11/tree-processor/blob/main/README.en.md) | [中文](https://github.com/knott11/tree-processor/blob/main/README.md)
6
+
7
+ </div>
8
+
9
+ <div align="center">
10
+
11
+ ![version](https://img.shields.io/npm/v/tree-processor?style=flat-square&label=version)
12
+ ![npm downloads (2 months)](https://img.shields.io/badge/downloads-1.3K%2F2mo-brightgreen?style=flat-square)
13
+ ![bundle size](https://img.shields.io/badge/bundle-8.4KB-blue?style=flat-square)
14
+ ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
15
+ ![coverage](https://img.shields.io/badge/coverage-99%25-brightgreen?style=flat-square)
16
+
17
+ 一个轻量级的树结构数据处理工具库,使用 TypeScript 编写,支持 tree-shaking,每个格式打包体积约 **8.2-8.5 KB**(ESM: 8.24 KB,CJS: 8.51 KB,UMD: 8.52 KB)。
18
+
19
+
20
+ </div>
21
+
22
+ ## 📋 目录
23
+
24
+ - [特性](#-特性)
25
+ - [使用场景](#-使用场景)
26
+ - [安装](#-安装)
27
+ - [快速开始](#-快速开始)
28
+ - [API 文档](#-api-文档)
29
+ - [遍历方法](#遍历方法)
30
+ - [查找方法](#查找方法)
31
+ - [访问方法](#访问方法)
32
+ - [修改方法](#修改方法)
33
+ - [转换方法](#转换方法)
34
+ - [查询方法](#查询方法)
35
+ - [验证方法](#验证方法)
36
+ - [自定义字段名](#自定义字段名)
37
+ - [测试](#测试)
38
+ - [开发](#开发)
39
+
40
+ ## ✨ 特性
41
+
42
+ - **轻量级** - 每个格式打包体积仅 8.2-8.5 KB(ESM: 8.24 KB,CJS: 8.51 KB,UMD: 8.52 KB),对项目体积影响极小
43
+ - **支持 Tree-shaking** - 支持按需导入,只打包实际使用的代码,进一步减小打包体积
44
+ - **完整的 TypeScript 支持** - 提供完整的类型定义和智能提示,提升开发体验
45
+ - **灵活的自定义字段名** - 支持自定义 children 和 id 字段名,适配各种数据结构
46
+ - **零依赖** - 无任何外部依赖,开箱即用,无需担心依赖冲突
47
+ - **完善的测试覆盖** - 包含 328 个测试用例,测试覆盖率达到 99%+(语句覆盖率 99%,分支覆盖率 98.41%,函数覆盖率 100%,行覆盖率 98.99%),覆盖基础功能、边界情况、异常处理、复杂场景等
48
+ - **丰富的 API** - 提供 32+ 个方法,包含类似数组的 API(map、filter、find、some、every等),以及树结构特有的操作(获取父子节点、深度计算、数据验证、格式转换等),涵盖遍历、查找、修改、转换、判断等完整场景
49
+
50
+ **已支持的方法:** mapTree、forEachTree、filterTree、findTree、pushTree、unshiftTree、popTree、shiftTree、someTree、everyTree、includesTree、atTree、indexOfTree、atIndexOfTree、dedupTree、removeTree、getParentTree、getChildrenTree、getSiblingsTree、getNodeDepthMap、getNodeDepth、isLeafNode、isRootNode、isEmptyTreeData、isEmptySingleTreeData、isTreeData、isSingleTreeData、isValidTreeNode、isTreeNodeWithCircularCheck、isSafeTreeDepth、convertToArrayTree、convertBackTree、convertToMapTree、convertToLevelArrayTree、convertToObjectTree。每个方法的最后一个参数可以自定义 children 和 id 的属性名。
51
+
52
+ ### 💡 使用场景
53
+
54
+ - **导航系统** - 多级菜单、路由配置的展开、折叠、搜索、过滤
55
+ - **文件系统** - 文件目录的遍历、查找、移动、删除
56
+ - **权限系统** - 组织架构、角色权限的树形结构管理和验证
57
+ - **框架开发** - 组件树、路由树等树形结构的构建和管理
58
+ - **数据管理** - 分类管理、评论系统、树形选择器等数据操作
59
+
60
+ ## 📦 安装
61
+
62
+ ```bash
63
+ npm install tree-processor
64
+ # 或
65
+ yarn add tree-processor
66
+ # 或
67
+ pnpm add tree-processor
68
+ ```
69
+
70
+ ## 🚀 快速开始
71
+
72
+ ```javascript
73
+ import { mapTree, findTree, filterTree } from 'tree-processor'
74
+
75
+ const treeData = [
76
+ {
77
+ id: 1,
78
+ name: 'node1',
79
+ children: [
80
+ { id: 2, name: 'node2' },
81
+ { id: 3, name: 'node3' },
82
+ ],
83
+ },
84
+ ]
85
+
86
+ // 获取所有节点名称
87
+ const names = mapTree(treeData, (node) => node.name)
88
+ console.log(names) // ['node1', 'node2', 'node3']
89
+
90
+ // 查找节点
91
+ const node = findTree(treeData, (n) => n.id === 2)
92
+ console.log(node) // { id: 2, name: 'node2' }
93
+
94
+ // 过滤节点
95
+ const filtered = filterTree(treeData, (n) => n.id > 1)
96
+ console.log(filtered) // [{ id: 2, name: 'node2' }, { id: 3, name: 'node3' }]
97
+ ```
98
+
99
+ ## 📖 API 文档
100
+
101
+ ### 引入方式
102
+
103
+ #### 默认导入(推荐用于需要多个方法的场景)
104
+
105
+ ```javascript
106
+ // ES Module
107
+ import t from 'tree-processor'
108
+
109
+ // CommonJS
110
+ const t = require('tree-processor')
111
+ ```
112
+
113
+ #### 按需导入(推荐用于只需要少量方法的场景,支持 tree-shaking)
114
+
115
+ ```javascript
116
+ // ES Module - 按需导入单个方法
117
+ import { mapTree, filterTree, findTree } from 'tree-processor'
118
+
119
+ // ES Module - 按需导入类型
120
+ import type { TreeNode, TreeData, FieldNames } from 'tree-processor'
121
+
122
+ // CommonJS - 按需导入
123
+ const { mapTree, filterTree } = require('tree-processor')
124
+ ```
125
+
126
+ ### 示例数据
127
+
128
+ 以下示例数据将用于后续所有方法的演示:
129
+
130
+ ```javascript
131
+ const treeData = [
132
+ {
133
+ id: 1,
134
+ name: 'node1',
135
+ children: [
136
+ {
137
+ id: 2,
138
+ name: 'node2',
139
+ children: [
140
+ { id: 4, name: 'node4' },
141
+ { id: 5, name: 'node5' },
142
+ ],
143
+ },
144
+ {
145
+ id: 3,
146
+ name: 'node3',
147
+ children: [{ id: 6, name: 'node6' }],
148
+ },
149
+ ],
150
+ },
151
+ ];
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 遍历方法
157
+
158
+ ### mapTree
159
+
160
+ 遍历树结构数据,对每个节点执行回调函数,返回映射后的数组。
161
+
162
+ ```javascript
163
+ // 获取所有节点的名称
164
+ const nodeNames = t.mapTree(treeData, (node) => node.name)
165
+ console.log(nodeNames) // ['node1', 'node2', 'node4', 'node5', 'node3', 'node6']
166
+
167
+ // 获取所有节点的ID
168
+ const nodeIds = t.mapTree(treeData, (node) => node.id)
169
+ console.log(nodeIds) // [1, 2, 4, 5, 3, 6]
170
+
171
+ // 修改节点数据
172
+ const modifiedNodes = t.mapTree(treeData, (node) => ({
173
+ ...node,
174
+ label: node.name
175
+ }))
176
+ console.log(modifiedNodes) // 返回包含 label 字段的新数组
177
+ ```
178
+
179
+ ### forEachTree
180
+
181
+ 遍历树结构数据,对每个节点执行回调函数。与 mapTree 的区别是不返回值,性能更好,适合只需要遍历而不需要返回结果的场景。
182
+
183
+ ```javascript
184
+ // 遍历所有节点并打印
185
+ t.forEachTree(treeData, (node) => {
186
+ console.log(node)
187
+ })
188
+
189
+ // 修改节点属性
190
+ t.forEachTree(treeData, (node) => {
191
+ node.visited = true
192
+ node.timestamp = Date.now()
193
+ })
194
+
195
+ // 统计节点数量
196
+ let nodeCount = 0
197
+ t.forEachTree(treeData, () => {
198
+ nodeCount++
199
+ })
200
+ console.log(nodeCount) // 节点总数
201
+ ```
202
+
203
+ ---
204
+
205
+ ## 查找方法
206
+
207
+ ### filterTree
208
+
209
+ 过滤树结构数据,返回满足条件的节点。
210
+
211
+ ```javascript
212
+ // 过滤出名称包含 'node' 的节点
213
+ const filteredNodes = t.filterTree(treeData, (node) => {
214
+ return node.name.includes('node')
215
+ })
216
+ console.log(filteredNodes) // 返回满足条件的节点数组
217
+
218
+ // 过滤出ID大于2的节点
219
+ const nodesWithLargeId = t.filterTree(treeData, (node) => node.id > 2)
220
+ console.log(nodesWithLargeId) // 返回ID大于2的节点数组
221
+
222
+ // 过滤出没有子节点的节点(叶子节点)
223
+ const leafNodes = t.filterTree(treeData, (node) => {
224
+ return !node.children || node.children.length === 0
225
+ })
226
+ console.log(leafNodes) // 返回所有叶子节点
227
+ ```
228
+
229
+ ### findTree
230
+
231
+ 查找树结构数据中满足条件的第一个节点。如果未找到,返回 null。
232
+
233
+ ```javascript
234
+ // 查找ID为2的节点
235
+ const foundNode = t.findTree(treeData, (node) => node.id === 2)
236
+ console.log(foundNode) // 返回找到的节点对象,未找到返回 null
237
+
238
+ // 查找名称为 'node3' 的节点
239
+ const node3 = t.findTree(treeData, (node) => node.name === 'node3')
240
+ console.log(node3) // { id: 3, name: 'node3', children: [...] }
241
+
242
+ // 查找不存在的节点
243
+ const nodeNotFound = t.findTree(treeData, (node) => node.id === 999)
244
+ console.log(nodeNotFound) // null
245
+ ```
246
+
247
+ ### includesTree
248
+
249
+ 检查树结构数据中是否包含指定ID的节点。
250
+
251
+ ```javascript
252
+ const nodeId = 2
253
+ const hasNode = t.includesTree(treeData, nodeId)
254
+
255
+ console.log(hasNode) // true 表示包含该节点,false 表示不包含
256
+ ```
257
+
258
+ ### someTree
259
+
260
+ 检查树结构数据中是否存在满足条件的节点。只要有一个节点满足条件就返回 true。
261
+
262
+ ```javascript
263
+ // 检查是否存在名称为 'node2' 的节点
264
+ const hasNode2 = t.someTree(treeData, node => node.name === 'node2')
265
+ console.log(hasNode2) // true
266
+
267
+ // 检查是否存在ID大于10的节点
268
+ const hasLargeId = t.someTree(treeData, node => node.id > 10)
269
+ console.log(hasLargeId) // false
270
+ ```
271
+
272
+ ### everyTree
273
+
274
+ 检查树结构数据中是否所有节点都满足条件。只有所有节点都满足条件才返回 true。
275
+
276
+ ```javascript
277
+ // 检查所有节点的ID是否都大于0
278
+ const allIdsPositive = t.everyTree(treeData, node => node.id > 0)
279
+ console.log(allIdsPositive) // true
280
+
281
+ // 检查所有节点是否都有 name 属性
282
+ const allHaveName = t.everyTree(treeData, node => node.name)
283
+ console.log(allHaveName) // 根据实际数据返回 true 或 false
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 访问方法
289
+
290
+ ### atTree
291
+
292
+ 根据父节点ID和子节点索引获取节点。支持负数索引,和数组的 at 方法一样。未找到返回 null。
293
+
294
+ ```javascript
295
+ // 获取ID为1的节点的第一个子节点(索引0)
296
+ const firstChildNode = t.atTree(treeData, 1, 0)
297
+ console.log(firstChildNode) // 返回第一个子节点
298
+
299
+ // 获取最后一个子节点(负数索引)
300
+ const lastChildNode = t.atTree(treeData, 1, -1)
301
+ console.log(lastChildNode) // 返回最后一个子节点
302
+
303
+ // 索引超出范围返回 null
304
+ const nodeNotFound = t.atTree(treeData, 1, 10)
305
+ console.log(nodeNotFound) // null
306
+ ```
307
+
308
+ ### indexOfTree
309
+
310
+ 返回一个数组,值为从根节点开始到 targetId 所在节点的索引路径。未找到返回 null。返回值可以传入 atIndexOfTree 的第二个参数进行取值。
311
+
312
+ ```javascript
313
+ // 获取ID为4的节点的索引路径
314
+ const nodePath = t.indexOfTree(treeData, 4)
315
+ console.log(nodePath) // [0, 0, 0] 表示根节点 -> 第一个子节点 -> 第一个子节点
316
+
317
+ // 未找到节点返回 null
318
+ const pathNotFound = t.indexOfTree(treeData, 999)
319
+ console.log(pathNotFound) // null
320
+
321
+ // 结合 atIndexOfTree 使用
322
+ const indexPath = t.indexOfTree(treeData, 4)
323
+ const nodeByPath = t.atIndexOfTree(treeData, indexPath)
324
+ console.log(nodeByPath) // 获取到ID为4的节点
325
+ ```
326
+
327
+ ### atIndexOfTree
328
+
329
+ 根据索引路径获取节点。路径无效或超出范围返回 null。
330
+
331
+ ```javascript
332
+ // 根据索引路径获取节点
333
+ const nodeByIndexPath = t.atIndexOfTree(treeData, [0, 1, 0])
334
+ console.log(nodeByIndexPath) // 返回对应路径的节点对象
335
+
336
+ // 结合 indexOfTree 使用
337
+ const targetPath = t.indexOfTree(treeData, 4)
338
+ const targetNode = t.atIndexOfTree(treeData, targetPath)
339
+ console.log(targetNode) // 获取到ID为4的节点
340
+
341
+ // 路径无效返回 null
342
+ const invalidPath = t.atIndexOfTree(treeData, [999])
343
+ console.log(invalidPath) // null
344
+ ```
345
+
346
+ ---
347
+
348
+ ## 修改方法
349
+
350
+ ### pushTree
351
+
352
+ 在指定节点下添加子节点到末尾。返回 true 表示添加成功,false 表示未找到目标节点。
353
+
354
+ ```javascript
355
+ // 在ID为1的节点下添加新子节点
356
+ const addSuccess = t.pushTree(treeData, 1, { id: 7, name: 'node7' })
357
+ console.log(addSuccess) // true
358
+ console.log(treeData) // 新节点已添加到 children 数组末尾
359
+
360
+ // 尝试在不存在的节点下添加
361
+ const addFailed = t.pushTree(treeData, 999, { id: 8, name: 'node8' })
362
+ console.log(addFailed) // false,未找到目标节点
363
+ ```
364
+
365
+ ### unshiftTree
366
+
367
+ 在指定节点下添加子节点到开头。返回 true 表示添加成功,false 表示未找到目标节点。
368
+
369
+ ```javascript
370
+ // 在ID为1的节点下添加新子节点到开头
371
+ const unshiftSuccess = t.unshiftTree(treeData, 1, { id: 7, name: 'node7' })
372
+ console.log(unshiftSuccess) // true
373
+ console.log(treeData) // 新节点已添加到 children 数组开头
374
+ ```
375
+
376
+ ### popTree
377
+
378
+ 删除指定节点下的最后一个子节点。返回被删除的节点,如果节点不存在或没有子节点则返回 false。
379
+
380
+ ```javascript
381
+ // 删除ID为1的节点下的最后一个子节点
382
+ const removedNode = t.popTree(treeData, 1)
383
+ console.log(removedNode) // 返回被删除的节点对象,或 false
384
+
385
+ // 尝试删除不存在的节点下的子节点
386
+ const popFailed = t.popTree(treeData, 999)
387
+ console.log(popFailed) // false
388
+ ```
389
+
390
+ ### shiftTree
391
+
392
+ 删除指定节点下的第一个子节点。返回被删除的节点,如果节点不存在或没有子节点则返回 false。
393
+
394
+ ```javascript
395
+ // 删除ID为1的节点下的第一个子节点
396
+ const shiftedNode = t.shiftTree(treeData, 1)
397
+ console.log(shiftedNode) // 返回被删除的节点对象,或 false
398
+ ```
399
+
400
+ ### removeTree
401
+
402
+ 删除树结构数据中指定ID的节点,包括根节点和子节点。
403
+
404
+ ```javascript
405
+ const nodeIdToRemove = 2
406
+ const removeSuccess = t.removeTree(treeData, nodeIdToRemove)
407
+
408
+ console.log(removeSuccess) // true 表示删除成功,false 表示未找到节点
409
+ console.log(treeData) // 删除后的树结构
410
+ ```
411
+
412
+ ### dedupTree
413
+
414
+ 树结构对象数组去重方法,根据指定的键去除重复节点。保留第一次出现的节点。
415
+
416
+ ```javascript
417
+ // 根据 id 字段去重
418
+ const uniqueTreeData = t.dedupTree(treeData, 'id')
419
+ console.log(uniqueTreeData) // 返回去重后的树结构数据
420
+
421
+ // 根据 name 字段去重
422
+ const uniqueByNameTree = t.dedupTree(treeData, 'name')
423
+ console.log(uniqueByNameTree) // 返回根据 name 去重后的数据
424
+ ```
425
+
426
+ ---
427
+
428
+ ## 转换方法
429
+
430
+ ### convertToArrayTree
431
+
432
+ 将树结构数据扁平化为数组。返回的数组中每个节点都不包含 `children` 字段。
433
+
434
+ ```javascript
435
+ // 将树结构扁平化为数组
436
+ const array = t.convertToArrayTree(treeData)
437
+ console.log(array)
438
+ // [
439
+ // { id: 1, name: 'node1' },
440
+ // { id: 2, name: 'node2' },
441
+ // { id: 4, name: 'node4' },
442
+ // { id: 5, name: 'node5' },
443
+ // { id: 3, name: 'node3' },
444
+ // { id: 6, name: 'node6' }
445
+ // ]
446
+
447
+ // 注意:返回的节点不包含 children 字段
448
+ array.forEach(node => {
449
+ console.log(node.children) // undefined
450
+ })
451
+
452
+ // 支持自定义字段名
453
+ const customTree = [
454
+ {
455
+ nodeId: 1,
456
+ name: 'node1',
457
+ subNodes: [
458
+ { nodeId: 2, name: 'node2' }
459
+ ]
460
+ }
461
+ ]
462
+ const customArray = t.convertToArrayTree(customTree, {
463
+ children: 'subNodes',
464
+ id: 'nodeId'
465
+ })
466
+ console.log(customArray) // 扁平化后的数组,不包含 subNodes 字段
467
+ ```
468
+
469
+ ### convertToMapTree
470
+
471
+ 将树结构数据转换为 Map,key 为节点 ID,value 为节点对象(不包含 children 字段)。适用于需要快速通过 ID 查找节点的场景。
472
+
473
+ ```javascript
474
+ // 将树结构转换为 Map
475
+ const map = t.convertToMapTree(treeData)
476
+ console.log(map instanceof Map) // true
477
+ console.log(map.size) // 6
478
+
479
+ // 通过 ID 快速查找节点
480
+ const node = map.get(2)
481
+ console.log(node) // { id: 2, name: 'node2' }
482
+ console.log(node.children) // undefined(不包含 children 字段)
483
+
484
+ // 支持自定义字段名
485
+ const customTree = [
486
+ {
487
+ nodeId: 1,
488
+ name: 'node1',
489
+ subNodes: [
490
+ { nodeId: 2, name: 'node2' }
491
+ ]
492
+ }
493
+ ]
494
+ const customMap = t.convertToMapTree(customTree, {
495
+ children: 'subNodes',
496
+ id: 'nodeId'
497
+ })
498
+ console.log(customMap.get(1)) // { nodeId: 1, name: 'node1' }
499
+ ```
500
+
501
+ ### convertToLevelArrayTree
502
+
503
+ 将树结构数据转换为层级数组(二维数组),按深度分组。外层数组按深度索引,内层数组包含该深度的所有节点。
504
+
505
+ ```javascript
506
+ // 将树结构转换为层级数组
507
+ const levelArray = t.convertToLevelArrayTree(treeData)
508
+ console.log(levelArray)
509
+ // [
510
+ // [{ id: 1, name: 'node1' }], // 第 0 层
511
+ // [{ id: 2, name: 'node2' }, { id: 3, name: 'node3' }], // 第 1 层
512
+ // [{ id: 4, name: 'node4' }, { id: 5, name: 'node5' }, { id: 6, name: 'node6' }] // 第 2 层
513
+ // ]
514
+
515
+ // 遍历每一层
516
+ levelArray.forEach((level, depth) => {
517
+ console.log(`深度 ${depth}:`, level)
518
+ })
519
+
520
+ // 注意:返回的节点不包含 children 字段
521
+ levelArray[0][0].children // undefined
522
+
523
+ // 支持自定义字段名
524
+ const customTree = [
525
+ {
526
+ nodeId: 1,
527
+ name: 'node1',
528
+ subNodes: [
529
+ { nodeId: 2, name: 'node2' }
530
+ ]
531
+ }
532
+ ]
533
+ const customLevelArray = t.convertToLevelArrayTree(customTree, {
534
+ children: 'subNodes',
535
+ id: 'nodeId'
536
+ })
537
+ console.log(customLevelArray) // 按层级分组的数组
538
+ ```
539
+
540
+ ### convertToObjectTree
541
+
542
+ 将单根树结构数据转换为对象。如果树只有一个根节点,返回该节点对象;否则返回 `null`。
543
+
544
+ ```javascript
545
+ // 单根树转换为对象
546
+ const singleRootTree = [
547
+ {
548
+ id: 1,
549
+ name: 'node1',
550
+ value: 100,
551
+ children: [
552
+ { id: 2, name: 'node2' }
553
+ ]
554
+ }
555
+ ]
556
+ const rootNode = t.convertToObjectTree(singleRootTree)
557
+ console.log(rootNode)
558
+ // {
559
+ // id: 1,
560
+ // name: 'node1',
561
+ // value: 100,
562
+ // children: [{ id: 2, name: 'node2' }]
563
+ // }
564
+
565
+ // 多个根节点返回 null
566
+ const multiRootTree = [
567
+ { id: 1, name: 'node1' },
568
+ { id: 2, name: 'node2' }
569
+ ]
570
+ const result = t.convertToObjectTree(multiRootTree)
571
+ console.log(result) // null
572
+
573
+ // 空树返回 null
574
+ const emptyTree = []
575
+ const emptyResult = t.convertToObjectTree(emptyTree)
576
+ console.log(emptyResult) // null
577
+ ```
578
+
579
+ ### convertBackTree
580
+
581
+ 将各种数据结构转换为树结构数据。支持数组、Map、Record(对象)等格式。数组中的每个元素需要包含 `id` 和 `parentId` 字段。
582
+
583
+ ```javascript
584
+ // 将扁平数组转换为树结构
585
+ const array = [
586
+ { id: 1, name: 'node1', parentId: null },
587
+ { id: 2, name: 'node2', parentId: 1 },
588
+ { id: 3, name: 'node3', parentId: 1 },
589
+ { id: 4, name: 'node4', parentId: 2 },
590
+ { id: 5, name: 'node5', parentId: 2 },
591
+ { id: 6, name: 'node6', parentId: 3 }
592
+ ]
593
+ const tree = t.convertBackTree(array)
594
+ console.log(tree)
595
+ // [
596
+ // {
597
+ // id: 1,
598
+ // name: 'node1',
599
+ // children: [
600
+ // {
601
+ // id: 2,
602
+ // name: 'node2',
603
+ // children: [
604
+ // { id: 4, name: 'node4', children: [] },
605
+ // { id: 5, name: 'node5', children: [] }
606
+ // ]
607
+ // },
608
+ // {
609
+ // id: 3,
610
+ // name: 'node3',
611
+ // children: [
612
+ // { id: 6, name: 'node6', children: [] }
613
+ // ]
614
+ // }
615
+ // ]
616
+ // }
617
+ // ]
618
+
619
+ // 自定义根节点的 parentId 值
620
+ const arrayWithZero = [
621
+ { id: 1, name: 'node1', parentId: 0 },
622
+ { id: 2, name: 'node2', parentId: 1 }
623
+ ]
624
+ const treeWithZero = t.convertBackTree(arrayWithZero, { rootParentId: 0 })
625
+ console.log(treeWithZero) // 正确转换
626
+
627
+ // 自定义 parentId 字段名
628
+ const arrayWithPid = [
629
+ { id: 1, name: 'node1', pid: null },
630
+ { id: 2, name: 'node2', pid: 1 }
631
+ ]
632
+ const treeWithPid = t.convertBackTree(arrayWithPid, { parentIdField: 'pid' })
633
+ console.log(treeWithPid) // 正确转换
634
+
635
+ // 支持自定义字段名
636
+ const customArray = [
637
+ { nodeId: 1, name: 'node1', parentId: null },
638
+ { nodeId: 2, name: 'node2', parentId: 1 }
639
+ ]
640
+ const customTree = t.convertBackTree(customArray, {
641
+ fieldNames: { id: 'nodeId', children: 'subNodes' }
642
+ })
643
+ console.log(customTree)
644
+ // [
645
+ // {
646
+ // nodeId: 1,
647
+ // name: 'node1',
648
+ // subNodes: [
649
+ // { nodeId: 2, name: 'node2', subNodes: [] }
650
+ // ]
651
+ // }
652
+ // ]
653
+
654
+ // 处理多个根节点
655
+ const multiRootArray = [
656
+ { id: 1, name: 'root1', parentId: null },
657
+ { id: 2, name: 'root2', parentId: null },
658
+ { id: 3, name: 'child1', parentId: 1 }
659
+ ]
660
+ const multiRootTree = t.convertBackTree(multiRootArray)
661
+ console.log(multiRootTree) // 包含两个根节点
662
+ ```
663
+
664
+ **参数说明:**
665
+ - `data` - 支持多种数据格式:
666
+ - 数组:扁平数组,每个元素需要包含 `id` 和 `parentId` 字段
667
+ - Map:key 为节点 ID,value 为节点对象
668
+ - Record(对象):key 为节点 ID,value 为节点对象
669
+ - 单个对象:单个树节点对象
670
+ - `options.rootParentId` - 根节点的 parentId 值,默认为 `null`
671
+ - `options.parentIdField` - 父节点ID字段名,默认为 `'parentId'`
672
+ - `options.fieldNames` - 自定义字段名配置,支持自定义 `id` 和 `children` 字段名
673
+
674
+ **注意事项:**
675
+ - 如果节点的 `parentId` 找不到对应的父节点,该节点会被作为根节点处理
676
+ - 没有 `id` 的节点会被跳过
677
+ - `parentId` 为 `null`、`undefined` 或等于 `rootParentId` 的节点会被视为根节点
678
+ - Map 和 Record 格式转换时,key 会被设置为节点的 `id`
679
+
680
+ **示例:支持 Map 和 Record 格式**
681
+
682
+ ```javascript
683
+ // Map 格式
684
+ const map = new Map([
685
+ [1, { name: 'node1', parentId: null }],
686
+ [2, { name: 'node2', parentId: 1 }]
687
+ ])
688
+ const treeFromMap = t.convertBackTree(map)
689
+ console.log(treeFromMap) // 正确转换为树结构
690
+
691
+ // Record 格式
692
+ const record = {
693
+ 1: { name: 'node1', parentId: null },
694
+ 2: { name: 'node2', parentId: 1 }
695
+ }
696
+ const treeFromRecord = t.convertBackTree(record)
697
+ console.log(treeFromRecord) // 正确转换为树结构
698
+ ```
699
+
700
+ ---
701
+
702
+ ## 查询方法
703
+
704
+ ### getParentTree
705
+
706
+ 获取指定节点的父节点。如果节点是根节点或未找到,返回 null。
707
+
708
+ ```javascript
709
+ // 获取ID为2的节点的父节点
710
+ const parentNode = t.getParentTree(treeData, 2)
711
+ console.log(parentNode) // 返回父节点对象 { id: 1, name: 'node1', ... }
712
+
713
+ // 根节点没有父节点,返回 null
714
+ const rootParentNode = t.getParentTree(treeData, 1)
715
+ console.log(rootParentNode) // null
716
+
717
+ // 未找到节点返回 null
718
+ const parentNotFound = t.getParentTree(treeData, 999)
719
+ console.log(parentNotFound) // null
720
+ ```
721
+
722
+ ### getChildrenTree
723
+
724
+ 获取指定节点的所有直接子节点。如果未找到节点或没有子节点,返回空数组。
725
+
726
+ ```javascript
727
+ // 获取ID为1的节点的所有子节点
728
+ const children = t.getChildrenTree(treeData, 1)
729
+ console.log(children) // 返回子节点数组 [{ id: 2, ... }, { id: 3, ... }]
730
+
731
+ // 节点没有子节点,返回空数组
732
+ const emptyChildren = t.getChildrenTree(treeData, 4)
733
+ console.log(emptyChildren) // []
734
+
735
+ // 未找到节点返回空数组
736
+ const notFound = t.getChildrenTree(treeData, 999)
737
+ console.log(notFound) // []
738
+
739
+ // 支持自定义字段名
740
+ const customTree = [
741
+ {
742
+ nodeId: 1,
743
+ name: 'root',
744
+ subNodes: [
745
+ { nodeId: 2, name: 'child1' },
746
+ { nodeId: 3, name: 'child2' },
747
+ ],
748
+ },
749
+ ];
750
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
751
+ const customChildren = t.getChildrenTree(customTree, 1, fieldNames)
752
+ console.log(customChildren) // 返回子节点数组
753
+ ```
754
+
755
+ ### getSiblingsTree
756
+
757
+ 获取指定节点的所有兄弟节点(包括自己)。如果未找到节点,返回空数组。根节点的兄弟节点是其他根节点。
758
+
759
+ ```javascript
760
+ // 获取ID为2的节点的所有兄弟节点(包括自己)
761
+ const siblings = t.getSiblingsTree(treeData, 2)
762
+ console.log(siblings) // 返回兄弟节点数组 [{ id: 2, ... }, { id: 3, ... }]
763
+
764
+ // 根节点的兄弟节点是其他根节点
765
+ const multiRoot = [
766
+ { id: 1, children: [{ id: 2 }] },
767
+ { id: 3, children: [{ id: 4 }] },
768
+ ];
769
+ const rootSiblings = t.getSiblingsTree(multiRoot, 1)
770
+ console.log(rootSiblings) // 返回所有根节点 [{ id: 1, ... }, { id: 3, ... }]
771
+
772
+ // 未找到节点返回空数组
773
+ const notFound = t.getSiblingsTree(treeData, 999)
774
+ console.log(notFound) // []
775
+
776
+ // 支持自定义字段名
777
+ const customTree = [
778
+ {
779
+ nodeId: 1,
780
+ name: 'root',
781
+ subNodes: [
782
+ { nodeId: 2, name: 'child1' },
783
+ { nodeId: 3, name: 'child2' },
784
+ { nodeId: 4, name: 'child3' },
785
+ ],
786
+ },
787
+ ];
788
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
789
+ const customSiblings = t.getSiblingsTree(customTree, 2, fieldNames)
790
+ console.log(customSiblings) // 返回兄弟节点数组(包括自己)
791
+ ```
792
+
793
+ ### getNodeDepthMap
794
+
795
+ 返回一个字典,键代表节点的 id,值代表该节点在数据的第几层。深度从1开始,根节点深度为1。
796
+
797
+ ```javascript
798
+ // 获取所有节点的深度映射
799
+ const nodeDepthMap = t.getNodeDepthMap(treeData)
800
+ console.log(nodeDepthMap) // { 1: 1, 2: 2, 3: 2, 4: 3, 5: 3, 6: 3 }
801
+
802
+ // 获取特定节点的深度
803
+ const node2Depth = nodeDepthMap[2]
804
+ console.log(node2Depth) // 2
805
+
806
+ // 空树返回空对象
807
+ const emptyDepthMap = t.getNodeDepthMap([])
808
+ console.log(emptyDepthMap) // {}
809
+ ```
810
+
811
+ ### getNodeDepth
812
+
813
+ 获取指定节点的深度。深度从1开始,根节点深度为1。
814
+
815
+ ```javascript
816
+ // 获取根节点的深度
817
+ const rootDepth = t.getNodeDepth(treeData, 1)
818
+ console.log(rootDepth) // 1
819
+
820
+ // 获取子节点的深度
821
+ const childDepth = t.getNodeDepth(treeData, 2)
822
+ console.log(childDepth) // 2
823
+
824
+ // 获取深层节点的深度
825
+ const deepDepth = t.getNodeDepth(treeData, 4)
826
+ console.log(deepDepth) // 3
827
+
828
+ // 未找到节点返回 null
829
+ const notFound = t.getNodeDepth(treeData, 999)
830
+ console.log(notFound) // null
831
+
832
+ // 支持自定义字段名
833
+ const customTree = [
834
+ {
835
+ nodeId: 1,
836
+ name: 'root',
837
+ subNodes: [
838
+ { nodeId: 2, name: 'child' },
839
+ ],
840
+ },
841
+ ];
842
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
843
+ const depth = t.getNodeDepth(customTree, 2, fieldNames)
844
+ console.log(depth) // 2
845
+ ```
846
+
847
+ **与 getNodeDepthMap 的区别:**
848
+ - `getNodeDepthMap` - 批量获取所有节点的深度(一次性计算所有节点)
849
+ - `getNodeDepth` - 只获取单个节点的深度(只计算目标节点,效率更高)
850
+
851
+ ---
852
+
853
+ ## 验证方法
854
+
855
+ ### isLeafNode
856
+
857
+ 检查节点是否是叶子节点(没有子节点)。轻量级方法,只检查节点本身,不遍历树。
858
+
859
+ ```javascript
860
+ // 没有 children 字段的节点是叶子节点
861
+ const leafNode1 = { id: 1, name: 'node1' };
862
+ console.log(t.isLeafNode(leafNode1)) // true
863
+
864
+ // children 为空数组的节点是叶子节点
865
+ const leafNode2 = { id: 2, name: 'node2', children: [] };
866
+ console.log(t.isLeafNode(leafNode2)) // true
867
+
868
+ // 有子节点的节点不是叶子节点
869
+ const parentNode = {
870
+ id: 3,
871
+ name: 'node3',
872
+ children: [{ id: 4, name: 'node4' }],
873
+ };
874
+ console.log(t.isLeafNode(parentNode)) // false
875
+
876
+ // 在 filterTree 中使用(过滤出所有叶子节点)
877
+ const leafNodes = t.filterTree(treeData, (node) => t.isLeafNode(node))
878
+ console.log(leafNodes) // 返回所有叶子节点
879
+
880
+ // 在 forEachTree 中使用
881
+ t.forEachTree(treeData, (node) => {
882
+ if (t.isLeafNode(node)) {
883
+ console.log('叶子节点:', node.name)
884
+ }
885
+ })
886
+
887
+ // 支持自定义字段名
888
+ const customNode = {
889
+ nodeId: 1,
890
+ name: 'node1',
891
+ subNodes: [],
892
+ };
893
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
894
+ console.log(t.isLeafNode(customNode, fieldNames)) // true
895
+ ```
896
+
897
+ **与现有方法的区别:**
898
+ - `isLeafNode` - 只检查单个节点,轻量级(O(1)),适合在遍历时使用
899
+ - `getChildrenTree` - 获取子节点数组,需要传入 tree 和 nodeId,需要查找节点(O(n))
900
+
901
+ ### isRootNode
902
+
903
+ 检查节点是否是根节点(没有父节点)。根节点是树结构数据数组中的顶层节点。
904
+
905
+ ```javascript
906
+ // 检查根节点
907
+ const treeData = [
908
+ {
909
+ id: 1,
910
+ name: 'root1',
911
+ children: [{ id: 2, name: 'child1' }],
912
+ },
913
+ ];
914
+ console.log(t.isRootNode(treeData, 1)) // true
915
+ console.log(t.isRootNode(treeData, 2)) // false
916
+
917
+ // 多个根节点的情况
918
+ const multiRoot = [
919
+ { id: 1, name: 'root1' },
920
+ { id: 2, name: 'root2' },
921
+ { id: 3, name: 'root3' },
922
+ ];
923
+ console.log(t.isRootNode(multiRoot, 1)) // true
924
+ console.log(t.isRootNode(multiRoot, 2)) // true
925
+ console.log(t.isRootNode(multiRoot, 3)) // true
926
+
927
+ // 在遍历时使用
928
+ t.forEachTree(treeData, (node) => {
929
+ if (t.isRootNode(treeData, node.id)) {
930
+ console.log('根节点:', node.name)
931
+ }
932
+ })
933
+
934
+ // 支持自定义字段名
935
+ const customTree = [
936
+ {
937
+ nodeId: 1,
938
+ name: 'root1',
939
+ subNodes: [{ nodeId: 2, name: 'child1' }],
940
+ },
941
+ ];
942
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
943
+ console.log(t.isRootNode(customTree, 1, fieldNames)) // true
944
+ console.log(t.isRootNode(customTree, 2, fieldNames)) // false
945
+
946
+ // 节点不存在时返回 false
947
+ console.log(t.isRootNode(treeData, 999)) // false
948
+ ```
949
+
950
+ **与现有方法的区别:**
951
+ - `isRootNode` - 语义化方法,直接返回布尔值
952
+ - `getParentTree` - 返回父节点对象,需要判断是否为 null
953
+ - `getNodeDepth` - 返回深度,需要判断是否等于 1
954
+
955
+ ### isEmptyTreeData
956
+
957
+ 检查树结构数据(数组)是否为空。空数组、null、undefined 都视为空。此函数支持 fieldNames 参数以保持 API 一致性,但该参数不生效(因为只检查数组是否为空,不访问 children 或 id 字段)。
958
+
959
+ ```javascript
960
+ // 检查树结构数据是否为空
961
+ const isEmptyTree = t.isEmptyTreeData(treeData)
962
+ console.log(isEmptyTree) // false(有数据)
963
+
964
+ // 空数组返回 true
965
+ const isEmptyArray = t.isEmptyTreeData([])
966
+ console.log(isEmptyArray) // true
967
+
968
+ // null 或 undefined 返回 true
969
+ const isNullTree = t.isEmptyTreeData(null)
970
+ console.log(isNullTree) // true
971
+
972
+ // 支持 fieldNames 参数(保持 API 一致性,但不生效)
973
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
974
+ const isEmptyWithFieldNames = t.isEmptyTreeData(treeData, fieldNames)
975
+ console.log(isEmptyWithFieldNames) // false(结果与不传 fieldNames 相同)
976
+ ```
977
+
978
+ ### isEmptySingleTreeData
979
+
980
+ 检查单个树结构数据是否为空。如果数据不是有效的单个树结构数据、没有 children 字段,或者 children 是空数组,则视为空。如果有子节点(children 数组不为空),即使子节点本身是空的,树也不为空。
981
+
982
+ ```javascript
983
+ // 没有 children 字段,视为空
984
+ const tree1 = { id: 1, name: 'node1' };
985
+ const isEmpty1 = t.isEmptySingleTreeData(tree1)
986
+ console.log(isEmpty1) // true
987
+
988
+ // children 是空数组,视为空
989
+ const tree2 = {
990
+ id: 1,
991
+ name: 'node1',
992
+ children: [],
993
+ };
994
+ const isEmpty2 = t.isEmptySingleTreeData(tree2)
995
+ console.log(isEmpty2) // true
996
+
997
+ // 有子节点,不为空
998
+ const tree3 = {
999
+ id: 1,
1000
+ name: 'node1',
1001
+ children: [
1002
+ { id: 2, name: 'node2' },
1003
+ ],
1004
+ };
1005
+ const isEmpty3 = t.isEmptySingleTreeData(tree3)
1006
+ console.log(isEmpty3) // false
1007
+
1008
+ // 有子节点,即使子节点本身是空的,树也不为空
1009
+ const tree4 = {
1010
+ id: 1,
1011
+ name: 'node1',
1012
+ children: [
1013
+ { id: 2, name: 'node2', children: [] },
1014
+ { id: 3, name: 'node3' }, // 没有children字段
1015
+ ],
1016
+ };
1017
+ const isEmpty4 = t.isEmptySingleTreeData(tree4)
1018
+ console.log(isEmpty4) // false(因为有子节点,即使子节点是空的)
1019
+
1020
+ // 支持自定义字段名
1021
+ const customTree = {
1022
+ nodeId: 1,
1023
+ name: 'node1',
1024
+ subNodes: [],
1025
+ };
1026
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1027
+ const isEmptyCustom = t.isEmptySingleTreeData(customTree, fieldNames)
1028
+ console.log(isEmptyCustom) // true
1029
+ ```
1030
+
1031
+ ### isTreeData
1032
+
1033
+ 判断数据是否是树结构数据(数组)。树结构数据必须是一个数组,数组中的每个元素都必须是有效的单个树结构数据。
1034
+
1035
+ ```javascript
1036
+ // 有效的树结构数据(森林)
1037
+ const forest = [
1038
+ {
1039
+ id: 1,
1040
+ name: 'node1',
1041
+ children: [{ id: 2, name: 'node2' }],
1042
+ },
1043
+ {
1044
+ id: 3,
1045
+ name: 'node3',
1046
+ children: [{ id: 4, name: 'node4' }],
1047
+ },
1048
+ ];
1049
+ console.log(t.isTreeData(forest)) // true
1050
+
1051
+ // 空数组也是有效的树结构数据(空森林)
1052
+ console.log(t.isTreeData([])) // true
1053
+
1054
+ // 单个对象不是树结构数据(应该用 isSingleTreeData)
1055
+ console.log(t.isTreeData({ id: 1 })) // false
1056
+
1057
+ // 数组包含非树结构元素,返回 false
1058
+ const invalidForest = [
1059
+ { id: 1, children: [{ id: 2 }] },
1060
+ 'not a tree', // 无效元素
1061
+ ];
1062
+ console.log(t.isTreeData(invalidForest)) // false
1063
+
1064
+ // null 或 undefined 不是有效的树结构数据
1065
+ console.log(t.isTreeData(null)) // false
1066
+ console.log(t.isTreeData(undefined)) // false
1067
+
1068
+ // 支持自定义字段名
1069
+ const customForest = [
1070
+ {
1071
+ nodeId: 1,
1072
+ name: 'node1',
1073
+ subNodes: [{ nodeId: 2, name: 'node2' }],
1074
+ },
1075
+ ];
1076
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1077
+ console.log(t.isTreeData(customForest, fieldNames)) // true
1078
+ ```
1079
+
1080
+ ### isSingleTreeData
1081
+
1082
+ 判断数据是否是单个树结构数据(单个对象)。树结构数据必须是一个对象(不能是数组、null、undefined 或基本类型),如果存在 children 字段,必须是数组类型,并且会递归检查所有子节点。
1083
+
1084
+ ```javascript
1085
+ // 有效的单个树结构数据
1086
+ const tree = {
1087
+ id: 1,
1088
+ name: 'node1',
1089
+ children: [
1090
+ { id: 2, name: 'node2' },
1091
+ { id: 3, name: 'node3' },
1092
+ ],
1093
+ };
1094
+ const isValid = t.isSingleTreeData(tree)
1095
+ console.log(isValid) // true
1096
+
1097
+ // 没有 children 字段也是有效的(只有根节点)
1098
+ const singleNode = { id: 1, name: 'node1' }
1099
+ console.log(t.isSingleTreeData(singleNode)) // true
1100
+
1101
+ // 数组不是单个树结构数据
1102
+ console.log(t.isSingleTreeData([])) // false
1103
+
1104
+ // null 或 undefined 不是有效的树结构数据
1105
+ console.log(t.isSingleTreeData(null)) // false
1106
+ console.log(t.isSingleTreeData(undefined)) // false
1107
+
1108
+ // children 不能是 null
1109
+ const invalidTree = { id: 1, children: null }
1110
+ console.log(t.isSingleTreeData(invalidTree)) // false
1111
+
1112
+ // 支持自定义字段名
1113
+ const customTree = {
1114
+ nodeId: 1,
1115
+ name: 'node1',
1116
+ subNodes: [{ nodeId: 2, name: 'node2' }],
1117
+ };
1118
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1119
+ console.log(t.isSingleTreeData(customTree, fieldNames)) // true
1120
+ ```
1121
+
1122
+ ### isValidTreeNode
1123
+
1124
+ 检查单个节点是否是有效的树节点结构(轻量级,不递归检查子节点)。只检查节点本身的结构,不检查子节点。
1125
+
1126
+ ```javascript
1127
+ // 有效的树节点(有 children 数组)
1128
+ const node1 = {
1129
+ id: 1,
1130
+ name: 'node1',
1131
+ children: [{ id: 2 }],
1132
+ };
1133
+ console.log(t.isValidTreeNode(node1)) // true
1134
+
1135
+ // 有效的树节点(没有 children 字段)
1136
+ const node2 = { id: 1, name: 'node1' };
1137
+ console.log(t.isValidTreeNode(node2)) // true
1138
+
1139
+ // 无效的树节点(children 不是数组)
1140
+ const invalidNode = {
1141
+ id: 1,
1142
+ children: 'not an array',
1143
+ };
1144
+ console.log(t.isValidTreeNode(invalidNode)) // false
1145
+
1146
+ // 支持自定义字段名
1147
+ const customNode = {
1148
+ nodeId: 1,
1149
+ subNodes: [{ nodeId: 2 }],
1150
+ };
1151
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1152
+ console.log(t.isValidTreeNode(customNode, fieldNames)) // true
1153
+ ```
1154
+
1155
+ **与 isSingleTreeData 的区别:**
1156
+ - `isValidTreeNode` - 只检查单个节点的基本结构,不递归检查子节点(轻量级)
1157
+ - `isSingleTreeData` - 递归检查整个树结构,确保所有子节点都是有效的树结构
1158
+
1159
+ ### isTreeNodeWithCircularCheck
1160
+
1161
+ 检查节点是否是有效的树节点结构,并检测循环引用。使用 WeakSet 跟踪已访问的节点,如果发现循环引用则返回 false。
1162
+
1163
+ ```javascript
1164
+ // 有效的树节点(无循环引用)
1165
+ const validNode = {
1166
+ id: 1,
1167
+ children: [
1168
+ { id: 2, children: [{ id: 3 }] },
1169
+ ],
1170
+ };
1171
+ console.log(t.isTreeNodeWithCircularCheck(validNode)) // true
1172
+
1173
+ // 检测循环引用
1174
+ const node1 = { id: 1, children: [] };
1175
+ const node2 = { id: 2, children: [] };
1176
+ node1.children.push(node2);
1177
+ node2.children.push(node1); // 循环引用
1178
+ console.log(t.isTreeNodeWithCircularCheck(node1)) // false
1179
+
1180
+ // 检测自引用
1181
+ const selfRefNode = { id: 1, children: [] };
1182
+ selfRefNode.children.push(selfRefNode); // 自引用
1183
+ console.log(t.isTreeNodeWithCircularCheck(selfRefNode)) // false
1184
+
1185
+ // 支持自定义字段名
1186
+ const customNode = {
1187
+ nodeId: 1,
1188
+ subNodes: [{ nodeId: 2 }],
1189
+ };
1190
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1191
+ console.log(t.isTreeNodeWithCircularCheck(customNode, fieldNames)) // true
1192
+ ```
1193
+
1194
+ **使用场景:**
1195
+ - 在接收用户输入或外部数据时,检查是否有循环引用
1196
+ - 数据验证,防止无限递归
1197
+ - 调试时检查数据结构是否正确
1198
+
1199
+ ### isSafeTreeDepth
1200
+
1201
+ 检查树结构数据的深度是否安全(防止递归爆栈)。如果树的深度超过 `maxDepth`,返回 false。
1202
+
1203
+ ```javascript
1204
+ // 深度安全的树
1205
+ const safeTree = [
1206
+ {
1207
+ id: 1,
1208
+ children: [
1209
+ { id: 2, children: [{ id: 3 }] },
1210
+ ],
1211
+ },
1212
+ ];
1213
+ console.log(t.isSafeTreeDepth(safeTree, 10)) // true(深度为3,小于10)
1214
+
1215
+ // 深度超过最大深度
1216
+ const deepTree = [
1217
+ {
1218
+ id: 1,
1219
+ children: [
1220
+ { id: 2, children: [{ id: 3 }] },
1221
+ ],
1222
+ },
1223
+ ];
1224
+ console.log(t.isSafeTreeDepth(deepTree, 2)) // false(深度为3,超过2)
1225
+
1226
+ // 空树总是安全的
1227
+ console.log(t.isSafeTreeDepth([], 10)) // true
1228
+
1229
+ // 单层树
1230
+ const singleLayer = [{ id: 1 }, { id: 2 }];
1231
+ console.log(t.isSafeTreeDepth(singleLayer, 1)) // true
1232
+
1233
+ // 支持自定义字段名
1234
+ const customTree = [
1235
+ {
1236
+ nodeId: 1,
1237
+ subNodes: [
1238
+ { nodeId: 2, subNodes: [{ nodeId: 3 }] },
1239
+ ],
1240
+ },
1241
+ ];
1242
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1243
+ console.log(t.isSafeTreeDepth(customTree, 3, fieldNames)) // true
1244
+ console.log(t.isSafeTreeDepth(customTree, 2, fieldNames)) // false
1245
+ ```
1246
+
1247
+ **使用场景:**
1248
+ - 在处理大型树之前,先检查深度是否安全
1249
+ - 防止递归调用栈溢出
1250
+ - 性能优化,避免处理过深的树结构
1251
+
1252
+ ## 自定义字段名
1253
+
1254
+ 所有方法都支持自定义 children 和 id 的属性名,通过最后一个参数传入配置对象:
1255
+
1256
+ ```javascript
1257
+ // 使用默认字段名
1258
+ const foundNode1 = t.findTree(treeData, (node) => node.id === 2)
1259
+
1260
+ // 使用自定义字段名
1261
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
1262
+ const foundNode2 = t.findTree(customTreeData, (node) => node.nodeId === 2, fieldNames);
1263
+ ```
1264
+
1265
+ **注意:** 所有 30 个函数都支持 `fieldNames` 参数,保持 API 一致性。即使某些函数(如 `isEmptyTreeData`)中该参数不生效,也可以传入以保持代码风格一致。
1266
+
1267
+ ## 测试
1268
+
1269
+ ### 运行测试
1270
+
1271
+ ```bash
1272
+ # 运行所有测试(自动打包后测试源码 + 打包文件,656 个测试用例)
1273
+ npm test
1274
+
1275
+ # 运行所有测试(单次,不监听文件变化)
1276
+ npm test -- --run
1277
+
1278
+ # 仅测试源代码(328 个测试用例)
1279
+ npm run test:src
1280
+
1281
+ # 仅测试打包后的文件(328 个测试用例,需要先运行 npm run build)
1282
+ npm run test:dist
1283
+
1284
+ # 运行测试并生成覆盖率报告
1285
+ npm run test:coverage
1286
+ ```
1287
+
1288
+ ## 开发
1289
+
1290
+ ```bash
1291
+ # 安装依赖
1292
+ npm install
1293
+
1294
+ # 运行测试
1295
+ npm test
1296
+
1297
+ # 构建项目(先删除 dist 目录,然后重新打包)
1298
+ npm run build
1299
+ ```
1300
+
1301
+ <div align="center">
1302
+
1303
+ 如果这个项目对你有帮助,请给它一个 ⭐️
1304
+
1305
+ Made with by [knott11]
1306
+
1307
+ </div>