tree-processor 0.6.2 → 0.8.0

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  一个轻量级的树结构数据处理工具库,使用 TypeScript 编写,支持 tree-shaking,每个格式打包体积约 **3-4 KB**(ESM: 3.25 KB,CJS: 3.42 KB,UMD: 3.56 KB)。
4
4
 
5
- 目前已支持 mapTree、forEachTree、filterTree、findTree、pushTree、unshiftTree、popTree、shiftTree、someTree、everyTree、includesTree、atTree、indexOfTree、atIndexOfTree、getParentTree、nodeDepthMapdedupTreeremoveTreeisEmptyTreeisSingleTree 和 isMultipleTrees。每个方法的最后一个参数可以自定义 children 和 id 的属性名。
5
+ 目前已支持 mapTree、forEachTree、filterTree、findTree、pushTree、unshiftTree、popTree、shiftTree、someTree、everyTree、includesTree、atTree、indexOfTree、atIndexOfTree、dedupTree、removeTree、getParentTree、getChildrenTreegetSiblingsTreegetNodeDepthMapgetNodeDepthisLeafNode、isRootNode、isEmptyTreeData、isEmptySingleTreeData、isTreeData、isSingleTreeData、isValidTreeNode、isTreeNodeWithCircularCheck、和isSafeTreeDepth。每个方法的最后一个参数可以自定义 children 和 id 的属性名。
6
6
 
7
7
  ## ✨ 特性
8
8
 
@@ -12,7 +12,7 @@
12
12
  - 🎯 **类似数组 API** - 提供 map、filter、find 等熟悉的数组方法
13
13
  - ⚙️ **自定义字段名** - 支持自定义 children 和 id 字段名
14
14
  - ✅ **零依赖** - 无外部依赖,开箱即用
15
- - 🧪 **完善的测试覆盖** - 包含 160 个测试用例,覆盖基础功能、边界情况、异常处理、复杂场景、npm 包导入等
15
+ - 🧪 **完善的测试覆盖** - 包含 272 个测试用例,覆盖基础功能、边界情况、异常处理、复杂场景、npm 包导入等
16
16
 
17
17
  ## 📦 安装
18
18
 
@@ -54,6 +54,11 @@ const { mapTree, filterTree } = require('tree-processor')
54
54
  - ✅ 更好的代码提示和类型检查
55
55
  - ✅ 更清晰的依赖关系
56
56
 
57
+ **关于类型导入:**
58
+ - TypeScript 会自动从函数签名推断类型,**大多数情况下不需要显式引入类型**
59
+ - 只有在需要显式声明变量类型时才需要引入类型(如 `const treeData: TreeData = [...]`)
60
+ - 使用 `import type` 导入类型不会增加运行时体积(类型在编译时会被移除)
61
+
57
62
  ### 示例树结构数据
58
63
 
59
64
  以下示例数据将用于后续所有方法的演示:
@@ -84,12 +89,23 @@ const treeData = [
84
89
 
85
90
  ### mapTree(遍历树结构数据的方法)
86
91
 
87
- 遍历树结构数据,对每个节点执行回调函数。
92
+ 遍历树结构数据,对每个节点执行回调函数,返回映射后的数组。
88
93
 
89
94
  ```javascript
90
- t.mapTree(treeData, (item) => {
91
- console.log(item)
92
- })
95
+ // 获取所有节点的名称
96
+ const nodeNames = t.mapTree(treeData, (node) => node.name)
97
+ console.log(nodeNames) // ['node1', 'node2', 'node4', 'node5', 'node3', 'node6']
98
+
99
+ // 获取所有节点的ID
100
+ const nodeIds = t.mapTree(treeData, (node) => node.id)
101
+ console.log(nodeIds) // [1, 2, 4, 5, 3, 6]
102
+
103
+ // 修改节点数据
104
+ const modifiedNodes = t.mapTree(treeData, (node) => ({
105
+ ...node,
106
+ label: node.name
107
+ }))
108
+ console.log(modifiedNodes) // 返回包含 label 字段的新数组
93
109
  ```
94
110
 
95
111
  ### forEachTree(遍历树结构数据的方法,不返回值)
@@ -97,11 +113,23 @@ t.mapTree(treeData, (item) => {
97
113
  遍历树结构数据,对每个节点执行回调函数。与 mapTree 的区别是不返回值,性能更好,适合只需要遍历而不需要返回结果的场景。
98
114
 
99
115
  ```javascript
100
- t.forEachTree(treeData, (item) => {
101
- console.log(item)
102
- // 可以在这里修改节点
103
- item.visited = true
116
+ // 遍历所有节点并打印
117
+ t.forEachTree(treeData, (node) => {
118
+ console.log(node)
119
+ })
120
+
121
+ // 修改节点属性
122
+ t.forEachTree(treeData, (node) => {
123
+ node.visited = true
124
+ node.timestamp = Date.now()
104
125
  })
126
+
127
+ // 统计节点数量
128
+ let nodeCount = 0
129
+ t.forEachTree(treeData, () => {
130
+ nodeCount++
131
+ })
132
+ console.log(nodeCount) // 节点总数
105
133
  ```
106
134
 
107
135
  ### filterTree(树结构数据的filter方法)
@@ -109,84 +137,117 @@ t.forEachTree(treeData, (item) => {
109
137
  过滤树结构数据,返回满足条件的节点。
110
138
 
111
139
  ```javascript
112
- const values = ['node1', 'node2', 'node3'];
113
- const result = t.filterTree(treeData, (item) => {
114
- return values.includes(item.name)
140
+ // 过滤出名称包含 'node' 的节点
141
+ const filteredNodes = t.filterTree(treeData, (node) => {
142
+ return node.name.includes('node')
115
143
  })
144
+ console.log(filteredNodes) // 返回满足条件的节点数组
116
145
 
117
- console.log(result)
146
+ // 过滤出ID大于2的节点
147
+ const nodesWithLargeId = t.filterTree(treeData, (node) => node.id > 2)
148
+ console.log(nodesWithLargeId) // 返回ID大于2的节点数组
149
+
150
+ // 过滤出没有子节点的节点(叶子节点)
151
+ const leafNodes = t.filterTree(treeData, (node) => {
152
+ return !node.children || node.children.length === 0
153
+ })
154
+ console.log(leafNodes) // 返回所有叶子节点
118
155
  ```
119
156
 
120
157
  ### findTree(树结构数据的find方法)
121
158
 
122
- 查找树结构数据中满足条件的第一个节点。
159
+ 查找树结构数据中满足条件的第一个节点。如果未找到,返回 null。
123
160
 
124
161
  ```javascript
125
- const result = t.findTree(treeData, (item) => {
126
- return item.hasOwnProperty('children')
127
- })
162
+ // 查找ID为2的节点
163
+ const foundNode = t.findTree(treeData, (node) => node.id === 2)
164
+ console.log(foundNode) // 返回找到的节点对象,未找到返回 null
165
+
166
+ // 查找名称为 'node3' 的节点
167
+ const node3 = t.findTree(treeData, (node) => node.name === 'node3')
168
+ console.log(node3) // { id: 3, name: 'node3', children: [...] }
128
169
 
129
- console.log(result)
170
+ // 查找不存在的节点
171
+ const nodeNotFound = t.findTree(treeData, (node) => node.id === 999)
172
+ console.log(nodeNotFound) // null
130
173
  ```
131
174
 
132
175
  ### pushTree(在指定节点下添加子节点到末尾)
133
176
 
134
- targetParentId 为目标节点的 id,newNode 为往该节点添加的数据。
177
+ 在指定节点下添加子节点到末尾。返回 true 表示添加成功,false 表示未找到目标节点。
135
178
 
136
179
  ```javascript
137
- t.pushTree(treeData, targetParentId, newNode);
138
-
139
- console.log(treeData)
180
+ // 在ID为1的节点下添加新子节点
181
+ const addSuccess = t.pushTree(treeData, 1, { id: 7, name: 'node7' })
182
+ console.log(addSuccess) // true
183
+ console.log(treeData) // 新节点已添加到 children 数组末尾
184
+
185
+ // 尝试在不存在的节点下添加
186
+ const addFailed = t.pushTree(treeData, 999, { id: 8, name: 'node8' })
187
+ console.log(addFailed) // false,未找到目标节点
140
188
  ```
141
189
 
142
190
  ### unshiftTree(在指定节点下添加子节点到开头)
143
191
 
144
- targetParentId 为目标节点的 id,newNode 为往该节点添加的数据。
192
+ 在指定节点下添加子节点到开头。返回 true 表示添加成功,false 表示未找到目标节点。
145
193
 
146
194
  ```javascript
147
- t.unshiftTree(treeData, targetParentId, newNode);
148
-
149
- console.log(treeData)
195
+ // 在ID为1的节点下添加新子节点到开头
196
+ const unshiftSuccess = t.unshiftTree(treeData, 1, { id: 7, name: 'node7' })
197
+ console.log(unshiftSuccess) // true
198
+ console.log(treeData) // 新节点已添加到 children 数组开头
150
199
  ```
151
200
 
152
201
  ### popTree(删除指定节点下的最后一个子节点)
153
202
 
154
- rootId 为目标节点的 id,此方法可删除 rootId 下的最后一个子节点。
203
+ 删除指定节点下的最后一个子节点。返回被删除的节点,如果节点不存在或没有子节点则返回 false。
155
204
 
156
205
  ```javascript
157
- t.popTree(treeData, rootId);
206
+ // 删除ID为1的节点下的最后一个子节点
207
+ const removedNode = t.popTree(treeData, 1)
208
+ console.log(removedNode) // 返回被删除的节点对象,或 false
158
209
 
159
- console.log(treeData)
210
+ // 尝试删除不存在的节点下的子节点
211
+ const popFailed = t.popTree(treeData, 999)
212
+ console.log(popFailed) // false
160
213
  ```
161
214
 
162
215
  ### shiftTree(删除指定节点下的第一个子节点)
163
216
 
164
- rootId 为目标节点的 id,此方法可删除 rootId 下的第一个子节点。
217
+ 删除指定节点下的第一个子节点。返回被删除的节点,如果节点不存在或没有子节点则返回 false。
165
218
 
166
219
  ```javascript
167
- t.shiftTree(treeData, rootId);
168
-
169
- console.log(treeData)
220
+ // 删除ID为1的节点下的第一个子节点
221
+ const shiftedNode = t.shiftTree(treeData, 1)
222
+ console.log(shiftedNode) // 返回被删除的节点对象,或 false
170
223
  ```
171
224
 
172
225
  ### someTree(树结构数据的some方法)
173
226
 
174
- 检查树结构数据中是否存在满足条件的节点。
227
+ 检查树结构数据中是否存在满足条件的节点。只要有一个节点满足条件就返回 true。
175
228
 
176
229
  ```javascript
177
- const result = t.someTree(treeData, item => item.name === 'jack')
230
+ // 检查是否存在名称为 'node2' 的节点
231
+ const hasNode2 = t.someTree(treeData, node => node.name === 'node2')
232
+ console.log(hasNode2) // true
178
233
 
179
- console.log(result)
234
+ // 检查是否存在ID大于10的节点
235
+ const hasLargeId = t.someTree(treeData, node => node.id > 10)
236
+ console.log(hasLargeId) // false
180
237
  ```
181
238
 
182
239
  ### everyTree(树结构数据的every方法)
183
240
 
184
- 检查树结构数据中是否所有节点都满足条件。
241
+ 检查树结构数据中是否所有节点都满足条件。只有所有节点都满足条件才返回 true。
185
242
 
186
243
  ```javascript
187
- const result = t.everyTree(treeData, item => item.age >= 18)
244
+ // 检查所有节点的ID是否都大于0
245
+ const allIdsPositive = t.everyTree(treeData, node => node.id > 0)
246
+ console.log(allIdsPositive) // true
188
247
 
189
- console.log(result)
248
+ // 检查所有节点是否都有 name 属性
249
+ const allHaveName = t.everyTree(treeData, node => node.name)
250
+ console.log(allHaveName) // 根据实际数据返回 true 或 false
190
251
  ```
191
252
 
192
253
  ### includesTree(检查树中是否包含指定节点)
@@ -194,146 +255,428 @@ console.log(result)
194
255
  检查树结构数据中是否包含指定ID的节点。
195
256
 
196
257
  ```javascript
197
- const hasNode = t.includesTree(treeData, targetId)
258
+ const nodeId = 2
259
+ const hasNode = t.includesTree(treeData, nodeId)
198
260
 
199
261
  console.log(hasNode) // true 表示包含该节点,false 表示不包含
200
262
  ```
201
263
 
202
264
  ### atTree(根据父节点ID和子节点索引获取节点)
203
265
 
204
- parentId 为指定父节点的 id,nodeIndex 为子节点的索引,可传负数,和数组的 at 方法一样。
266
+ 根据父节点ID和子节点索引获取节点。支持负数索引,和数组的 at 方法一样。未找到返回 null。
205
267
 
206
268
  ```javascript
207
- const result = t.atTree(treeData, parentId, nodeIndex)
269
+ // 获取ID为1的节点的第一个子节点(索引0)
270
+ const firstChildNode = t.atTree(treeData, 1, 0)
271
+ console.log(firstChildNode) // 返回第一个子节点
272
+
273
+ // 获取最后一个子节点(负数索引)
274
+ const lastChildNode = t.atTree(treeData, 1, -1)
275
+ console.log(lastChildNode) // 返回最后一个子节点
208
276
 
209
- console.log(result)
277
+ // 索引超出范围返回 null
278
+ const nodeNotFound = t.atTree(treeData, 1, 10)
279
+ console.log(nodeNotFound) // null
210
280
  ```
211
281
 
212
282
  ### indexOfTree(返回从根节点到目标节点的索引路径)
213
283
 
214
- 返回一个数组,值为从根节点开始到 targetId 所在节点的索引,返回值可以传入 atIndexOfTree 的第二个参数进行取值。
284
+ 返回一个数组,值为从根节点开始到 targetId 所在节点的索引路径。未找到返回 null。返回值可以传入 atIndexOfTree 的第二个参数进行取值。
215
285
 
216
286
  ```javascript
217
- const result = t.indexOfTree(treeData, targetId)
218
-
219
- console.log(result)
287
+ // 获取ID为4的节点的索引路径
288
+ const nodePath = t.indexOfTree(treeData, 4)
289
+ console.log(nodePath) // [0, 0, 0] 表示根节点 -> 第一个子节点 -> 第一个子节点
290
+
291
+ // 未找到节点返回 null
292
+ const pathNotFound = t.indexOfTree(treeData, 999)
293
+ console.log(pathNotFound) // null
294
+
295
+ // 结合 atIndexOfTree 使用
296
+ const indexPath = t.indexOfTree(treeData, 4)
297
+ const nodeByPath = t.atIndexOfTree(treeData, indexPath)
298
+ console.log(nodeByPath) // 获取到ID为4的节点
220
299
  ```
221
300
 
222
301
  ### atIndexOfTree(根据索引路径获取节点)
223
302
 
224
- 传入节点数据的下标数组,返回节点数据。
303
+ 根据索引路径获取节点。路径无效或超出范围返回 null。
225
304
 
226
305
  ```javascript
227
- const result = t.atIndexOfTree(treeData, [0, 1, 0])
306
+ // 根据索引路径获取节点
307
+ const nodeByIndexPath = t.atIndexOfTree(treeData, [0, 1, 0])
308
+ console.log(nodeByIndexPath) // 返回对应路径的节点对象
309
+
310
+ // 结合 indexOfTree 使用
311
+ const targetPath = t.indexOfTree(treeData, 4)
312
+ const targetNode = t.atIndexOfTree(treeData, targetPath)
313
+ console.log(targetNode) // 获取到ID为4的节点
314
+
315
+ // 路径无效返回 null
316
+ const invalidPath = t.atIndexOfTree(treeData, [999])
317
+ console.log(invalidPath) // null
318
+ ```
319
+
320
+ ### dedupTree(树结构对象数组去重方法)
321
+
322
+ 树结构对象数组去重方法,根据指定的键去除重复节点。保留第一次出现的节点。
228
323
 
229
- console.log(result)
324
+ ```javascript
325
+ // 根据 id 字段去重
326
+ const uniqueTreeData = t.dedupTree(treeData, 'id')
327
+ console.log(uniqueTreeData) // 返回去重后的树结构数据
328
+
329
+ // 根据 name 字段去重
330
+ const uniqueByNameTree = t.dedupTree(treeData, 'name')
331
+ console.log(uniqueByNameTree) // 返回根据 name 去重后的数据
332
+ ```
333
+
334
+ ### removeTree(删除指定节点)
335
+
336
+ 删除树结构数据中指定ID的节点,包括根节点和子节点。
337
+
338
+ ```javascript
339
+ const nodeIdToRemove = 2
340
+ const removeSuccess = t.removeTree(treeData, nodeIdToRemove)
341
+
342
+ console.log(removeSuccess) // true 表示删除成功,false 表示未找到节点
343
+ console.log(treeData) // 删除后的树结构
230
344
  ```
231
345
 
232
346
  ### getParentTree(获取节点的父节点)
233
347
 
234
- 获取指定节点的父节点。如果节点是根节点,返回 null。
348
+ 获取指定节点的父节点。如果节点是根节点或未找到,返回 null。
235
349
 
236
350
  ```javascript
237
- const parent = t.getParentTree(treeData, targetId)
351
+ // 获取ID为2的节点的父节点
352
+ const parentNode = t.getParentTree(treeData, 2)
353
+ console.log(parentNode) // 返回父节点对象 { id: 1, name: 'node1', ... }
354
+
355
+ // 根节点没有父节点,返回 null
356
+ const rootParentNode = t.getParentTree(treeData, 1)
357
+ console.log(rootParentNode) // null
238
358
 
239
- console.log(parent) // 返回父节点对象,如果未找到或节点是根节点则返回 null
359
+ // 未找到节点返回 null
360
+ const parentNotFound = t.getParentTree(treeData, 999)
361
+ console.log(parentNotFound) // null
240
362
  ```
241
363
 
242
- ### nodeDepthMap(返回节点ID到深度的映射)
364
+ ### getChildrenTree(获取节点的所有直接子节点)
243
365
 
244
- 返回一个字典,键代表节点的 id,值代表该节点在数据的第几层。
366
+ 获取指定节点的所有直接子节点。如果未找到节点或没有子节点,返回空数组。
245
367
 
246
368
  ```javascript
247
- const result = t.nodeDepthMap(treeData)
369
+ // 获取ID为1的节点的所有子节点
370
+ const children = t.getChildrenTree(treeData, 1)
371
+ console.log(children) // 返回子节点数组 [{ id: 2, ... }, { id: 3, ... }]
372
+
373
+ // 节点没有子节点,返回空数组
374
+ const emptyChildren = t.getChildrenTree(treeData, 4)
375
+ console.log(emptyChildren) // []
376
+
377
+ // 未找到节点返回空数组
378
+ const notFound = t.getChildrenTree(treeData, 999)
379
+ console.log(notFound) // []
248
380
 
249
- console.log(result)
381
+ // 支持自定义字段名
382
+ const customTree = [
383
+ {
384
+ nodeId: 1,
385
+ name: 'root',
386
+ subNodes: [
387
+ { nodeId: 2, name: 'child1' },
388
+ { nodeId: 3, name: 'child2' },
389
+ ],
390
+ },
391
+ ];
392
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
393
+ const customChildren = t.getChildrenTree(customTree, 1, fieldNames)
394
+ console.log(customChildren) // 返回子节点数组
250
395
  ```
251
396
 
252
- ### dedupTree(树结构对象数组去重方法)
397
+ ### getSiblingsTree(获取节点的所有兄弟节点)
253
398
 
254
- 树结构对象数组去重方法,第一个参数为需要去重的数据,第二个参数为以哪个键去重。
399
+ 获取指定节点的所有兄弟节点(包括自己)。如果未找到节点,返回空数组。根节点的兄弟节点是其他根节点。
255
400
 
256
401
  ```javascript
257
- const result = t.dedupTree(treeData, 'id')
402
+ // 获取ID为2的节点的所有兄弟节点(包括自己)
403
+ const siblings = t.getSiblingsTree(treeData, 2)
404
+ console.log(siblings) // 返回兄弟节点数组 [{ id: 2, ... }, { id: 3, ... }]
405
+
406
+ // 根节点的兄弟节点是其他根节点
407
+ const multiRoot = [
408
+ { id: 1, children: [{ id: 2 }] },
409
+ { id: 3, children: [{ id: 4 }] },
410
+ ];
411
+ const rootSiblings = t.getSiblingsTree(multiRoot, 1)
412
+ console.log(rootSiblings) // 返回所有根节点 [{ id: 1, ... }, { id: 3, ... }]
258
413
 
259
- console.log(result)
414
+ // 未找到节点返回空数组
415
+ const notFound = t.getSiblingsTree(treeData, 999)
416
+ console.log(notFound) // []
417
+
418
+ // 支持自定义字段名
419
+ const customTree = [
420
+ {
421
+ nodeId: 1,
422
+ name: 'root',
423
+ subNodes: [
424
+ { nodeId: 2, name: 'child1' },
425
+ { nodeId: 3, name: 'child2' },
426
+ { nodeId: 4, name: 'child3' },
427
+ ],
428
+ },
429
+ ];
430
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
431
+ const customSiblings = t.getSiblingsTree(customTree, 2, fieldNames)
432
+ console.log(customSiblings) // 返回兄弟节点数组(包括自己)
260
433
  ```
261
434
 
262
- ### removeTree(删除指定节点)
435
+ ### getNodeDepthMap(返回节点ID到深度的映射)
263
436
 
264
- 删除树结构数据中指定ID的节点,包括根节点和子节点。
437
+ 返回一个字典,键代表节点的 id,值代表该节点在数据的第几层。深度从1开始,根节点深度为1。
265
438
 
266
439
  ```javascript
267
- const success = t.removeTree(treeData, targetId)
440
+ // 获取所有节点的深度映射
441
+ const nodeDepthMap = t.getNodeDepthMap(treeData)
442
+ console.log(nodeDepthMap) // { 1: 1, 2: 2, 3: 2, 4: 3, 5: 3, 6: 3 }
268
443
 
269
- console.log(success) // true 表示删除成功,false 表示未找到节点
270
- console.log(treeData) // 删除后的树结构
444
+ // 获取特定节点的深度
445
+ const node2Depth = nodeDepthMap[2]
446
+ console.log(node2Depth) // 2
447
+
448
+ // 空树返回空对象
449
+ const emptyDepthMap = t.getNodeDepthMap([])
450
+ console.log(emptyDepthMap) // {}
271
451
  ```
272
452
 
273
- ### isEmptyTree(检查树是否为空)
453
+ ### getNodeDepth(获取单个节点的深度)
274
454
 
275
- 检查树结构数据是否为空。
455
+ 获取指定节点的深度。深度从1开始,根节点深度为1。
276
456
 
277
457
  ```javascript
278
- const isEmpty = t.isEmptyTree(treeData)
458
+ // 获取根节点的深度
459
+ const rootDepth = t.getNodeDepth(treeData, 1)
460
+ console.log(rootDepth) // 1
461
+
462
+ // 获取子节点的深度
463
+ const childDepth = t.getNodeDepth(treeData, 2)
464
+ console.log(childDepth) // 2
465
+
466
+ // 获取深层节点的深度
467
+ const deepDepth = t.getNodeDepth(treeData, 4)
468
+ console.log(deepDepth) // 3
469
+
470
+ // 未找到节点返回 null
471
+ const notFound = t.getNodeDepth(treeData, 999)
472
+ console.log(notFound) // null
279
473
 
280
- console.log(isEmpty) // true 表示树为空,false 表示树不为空
474
+ // 支持自定义字段名
475
+ const customTree = [
476
+ {
477
+ nodeId: 1,
478
+ name: 'root',
479
+ subNodes: [
480
+ { nodeId: 2, name: 'child' },
481
+ ],
482
+ },
483
+ ];
484
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
485
+ const depth = t.getNodeDepth(customTree, 2, fieldNames)
486
+ console.log(depth) // 2
281
487
  ```
282
488
 
283
- ### isSingleTree(判断数据是否是单个树结构)
489
+ **与 getNodeDepthMap 的区别:**
490
+ - `getNodeDepthMap` - 批量获取所有节点的深度(一次性计算所有节点)
491
+ - `getNodeDepth` - 只获取单个节点的深度(只计算目标节点,效率更高)
492
+
493
+ ### isLeafNode(检查节点是否是叶子节点)
284
494
 
285
- 判断数据是否是单个树结构(单个对象)。树结构必须是一个对象(不能是数组、null、undefined 或基本类型),如果存在 children 字段,必须是数组类型,并且会递归检查所有子节点。
495
+ 检查节点是否是叶子节点(没有子节点)。轻量级方法,只检查节点本身,不遍历树。
286
496
 
287
497
  ```javascript
288
- // 有效的单个树结构
289
- const tree = {
498
+ // 没有 children 字段的节点是叶子节点
499
+ const leafNode1 = { id: 1, name: 'node1' };
500
+ console.log(t.isLeafNode(leafNode1)) // true
501
+
502
+ // children 为空数组的节点是叶子节点
503
+ const leafNode2 = { id: 2, name: 'node2', children: [] };
504
+ console.log(t.isLeafNode(leafNode2)) // true
505
+
506
+ // 有子节点的节点不是叶子节点
507
+ const parentNode = {
508
+ id: 3,
509
+ name: 'node3',
510
+ children: [{ id: 4, name: 'node4' }],
511
+ };
512
+ console.log(t.isLeafNode(parentNode)) // false
513
+
514
+ // 在 filterTree 中使用(过滤出所有叶子节点)
515
+ const leafNodes = t.filterTree(treeData, (node) => t.isLeafNode(node))
516
+ console.log(leafNodes) // 返回所有叶子节点
517
+
518
+ // 在 forEachTree 中使用
519
+ t.forEachTree(treeData, (node) => {
520
+ if (t.isLeafNode(node)) {
521
+ console.log('叶子节点:', node.name)
522
+ }
523
+ })
524
+
525
+ // 支持自定义字段名
526
+ const customNode = {
527
+ nodeId: 1,
528
+ name: 'node1',
529
+ subNodes: [],
530
+ };
531
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
532
+ console.log(t.isLeafNode(customNode, fieldNames)) // true
533
+ ```
534
+
535
+ **与现有方法的区别:**
536
+ - `isLeafNode` - 只检查单个节点,轻量级(O(1)),适合在遍历时使用
537
+ - `getChildrenTree` - 获取子节点数组,需要传入 tree 和 nodeId,需要查找节点(O(n))
538
+
539
+ ### isRootNode(检查节点是否是根节点)
540
+
541
+ 检查节点是否是根节点(没有父节点)。根节点是树结构数据数组中的顶层节点。
542
+
543
+ ```javascript
544
+ // 检查根节点
545
+ const treeData = [
546
+ {
547
+ id: 1,
548
+ name: 'root1',
549
+ children: [{ id: 2, name: 'child1' }],
550
+ },
551
+ ];
552
+ console.log(t.isRootNode(treeData, 1)) // true
553
+ console.log(t.isRootNode(treeData, 2)) // false
554
+
555
+ // 多个根节点的情况
556
+ const multiRoot = [
557
+ { id: 1, name: 'root1' },
558
+ { id: 2, name: 'root2' },
559
+ { id: 3, name: 'root3' },
560
+ ];
561
+ console.log(t.isRootNode(multiRoot, 1)) // true
562
+ console.log(t.isRootNode(multiRoot, 2)) // true
563
+ console.log(t.isRootNode(multiRoot, 3)) // true
564
+
565
+ // 在遍历时使用
566
+ t.forEachTree(treeData, (node) => {
567
+ if (t.isRootNode(treeData, node.id)) {
568
+ console.log('根节点:', node.name)
569
+ }
570
+ })
571
+
572
+ // 支持自定义字段名
573
+ const customTree = [
574
+ {
575
+ nodeId: 1,
576
+ name: 'root1',
577
+ subNodes: [{ nodeId: 2, name: 'child1' }],
578
+ },
579
+ ];
580
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
581
+ console.log(t.isRootNode(customTree, 1, fieldNames)) // true
582
+ console.log(t.isRootNode(customTree, 2, fieldNames)) // false
583
+
584
+ // 节点不存在时返回 false
585
+ console.log(t.isRootNode(treeData, 999)) // false
586
+ ```
587
+
588
+ **与现有方法的区别:**
589
+ - `isRootNode` - 语义化方法,直接返回布尔值
590
+ - `getParentTree` - 返回父节点对象,需要判断是否为 null
591
+ - `getNodeDepth` - 返回深度,需要判断是否等于 1
592
+
593
+ ### isEmptyTreeData(检查树结构数据是否为空)
594
+
595
+ 检查树结构数据(数组)是否为空。空数组、null、undefined 都视为空。此函数支持 fieldNames 参数以保持 API 一致性,但该参数不生效(因为只检查数组是否为空,不访问 children 或 id 字段)。
596
+
597
+ ```javascript
598
+ // 检查树结构数据是否为空
599
+ const isEmptyTree = t.isEmptyTreeData(treeData)
600
+ console.log(isEmptyTree) // false(有数据)
601
+
602
+ // 空数组返回 true
603
+ const isEmptyArray = t.isEmptyTreeData([])
604
+ console.log(isEmptyArray) // true
605
+
606
+ // null 或 undefined 返回 true
607
+ const isNullTree = t.isEmptyTreeData(null)
608
+ console.log(isNullTree) // true
609
+
610
+ // 支持 fieldNames 参数(保持 API 一致性,但不生效)
611
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
612
+ const isEmptyWithFieldNames = t.isEmptyTreeData(treeData, fieldNames)
613
+ console.log(isEmptyWithFieldNames) // false(结果与不传 fieldNames 相同)
614
+ ```
615
+
616
+ ### isEmptySingleTreeData(检查单个树结构数据是否为空)
617
+
618
+ 检查单个树结构数据是否为空。如果数据不是有效的单个树结构数据、没有 children 字段,或者 children 是空数组,则视为空。如果有子节点(children 数组不为空),即使子节点本身是空的,树也不为空。
619
+
620
+ ```javascript
621
+ // 没有 children 字段,视为空
622
+ const tree1 = { id: 1, name: 'node1' };
623
+ const isEmpty1 = t.isEmptySingleTreeData(tree1)
624
+ console.log(isEmpty1) // true
625
+
626
+ // children 是空数组,视为空
627
+ const tree2 = {
628
+ id: 1,
629
+ name: 'node1',
630
+ children: [],
631
+ };
632
+ const isEmpty2 = t.isEmptySingleTreeData(tree2)
633
+ console.log(isEmpty2) // true
634
+
635
+ // 有子节点,不为空
636
+ const tree3 = {
290
637
  id: 1,
291
638
  name: 'node1',
292
639
  children: [
293
640
  { id: 2, name: 'node2' },
294
- { id: 3, name: 'node3' },
295
641
  ],
296
642
  };
643
+ const isEmpty3 = t.isEmptySingleTreeData(tree3)
644
+ console.log(isEmpty3) // false
297
645
 
298
- const isValid = t.isSingleTree(tree)
299
- console.log(isValid) // true
300
-
301
- // 无效的树结构
302
- const invalidTree = {
646
+ // 有子节点,即使子节点本身是空的,树也不为空
647
+ const tree4 = {
303
648
  id: 1,
304
- children: null, // children 不能是 null
649
+ name: 'node1',
650
+ children: [
651
+ { id: 2, name: 'node2', children: [] },
652
+ { id: 3, name: 'node3' }, // 没有children字段
653
+ ],
305
654
  };
306
-
307
- const isInvalid = t.isSingleTree(invalidTree)
308
- console.log(isInvalid) // false
655
+ const isEmpty4 = t.isEmptySingleTreeData(tree4)
656
+ console.log(isEmpty4) // false(因为有子节点,即使子节点是空的)
309
657
 
310
658
  // 支持自定义字段名
311
659
  const customTree = {
312
660
  nodeId: 1,
313
661
  name: 'node1',
314
- subNodes: [
315
- { nodeId: 2, name: 'node2' },
316
- ],
662
+ subNodes: [],
317
663
  };
318
-
319
664
  const fieldNames = { children: 'subNodes', id: 'nodeId' };
320
- const isValidCustom = t.isSingleTree(customTree, fieldNames)
321
- console.log(isValidCustom) // true
665
+ const isEmptyCustom = t.isEmptySingleTreeData(customTree, fieldNames)
666
+ console.log(isEmptyCustom) // true
322
667
  ```
323
668
 
324
- ### isMultipleTrees(判断数据是否是多个树结构)
669
+ ### isTreeData(判断数据是否是树结构数据)
325
670
 
326
- 判断数据是否是多个树结构(数组)。多个树结构必须是一个数组,数组中的每个元素都必须是有效的单个树结构。
671
+ 判断数据是否是树结构数据(数组)。树结构数据必须是一个数组,数组中的每个元素都必须是有效的单个树结构数据。
327
672
 
328
673
  ```javascript
329
- // 有效的多个树结构
674
+ // 有效的树结构数据(森林)
330
675
  const forest = [
331
676
  {
332
677
  id: 1,
333
678
  name: 'node1',
334
- children: [
335
- { id: 2, name: 'node2' },
336
- ],
679
+ children: [{ id: 2, name: 'node2' }],
337
680
  },
338
681
  {
339
682
  id: 3,
@@ -341,49 +684,224 @@ const forest = [
341
684
  children: [{ id: 4, name: 'node4' }],
342
685
  },
343
686
  ];
687
+ console.log(t.isTreeData(forest)) // true
344
688
 
345
- const isValid = t.isMultipleTrees(forest)
346
- console.log(isValid) // true
689
+ // 空数组也是有效的树结构数据(空森林)
690
+ console.log(t.isTreeData([])) // true
347
691
 
348
- // 空数组也是有效的多个树结构
349
- const emptyForest = []
350
- const isEmptyValid = t.isMultipleTrees(emptyForest)
351
- console.log(isEmptyValid) // true
692
+ // 单个对象不是树结构数据(应该用 isSingleTreeData)
693
+ console.log(t.isTreeData({ id: 1 })) // false
352
694
 
353
- // 无效的多个树结构
695
+ // 数组包含非树结构元素,返回 false
354
696
  const invalidForest = [
355
697
  { id: 1, children: [{ id: 2 }] },
356
- 'not a tree', // 数组元素必须是树结构
698
+ 'not a tree', // 无效元素
357
699
  ];
700
+ console.log(t.isTreeData(invalidForest)) // false
358
701
 
359
- const isInvalid = t.isMultipleTrees(invalidForest)
360
- console.log(isInvalid) // false
702
+ // null undefined 不是有效的树结构数据
703
+ console.log(t.isTreeData(null)) // false
704
+ console.log(t.isTreeData(undefined)) // false
361
705
 
362
706
  // 支持自定义字段名
363
707
  const customForest = [
364
708
  {
365
709
  nodeId: 1,
366
710
  name: 'node1',
367
- subNodes: [
368
- { nodeId: 2, name: 'node2' },
711
+ subNodes: [{ nodeId: 2, name: 'node2' }],
712
+ },
713
+ ];
714
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
715
+ console.log(t.isTreeData(customForest, fieldNames)) // true
716
+ ```
717
+
718
+ ### isSingleTreeData(判断数据是否是单个树结构数据)
719
+
720
+ 判断数据是否是单个树结构数据(单个对象)。树结构数据必须是一个对象(不能是数组、null、undefined 或基本类型),如果存在 children 字段,必须是数组类型,并且会递归检查所有子节点。
721
+
722
+ ```javascript
723
+ // 有效的单个树结构数据
724
+ const tree = {
725
+ id: 1,
726
+ name: 'node1',
727
+ children: [
728
+ { id: 2, name: 'node2' },
729
+ { id: 3, name: 'node3' },
730
+ ],
731
+ };
732
+ const isValid = t.isSingleTreeData(tree)
733
+ console.log(isValid) // true
734
+
735
+ // 没有 children 字段也是有效的(只有根节点)
736
+ const singleNode = { id: 1, name: 'node1' }
737
+ console.log(t.isSingleTreeData(singleNode)) // true
738
+
739
+ // 数组不是单个树结构数据
740
+ console.log(t.isSingleTreeData([])) // false
741
+
742
+ // null 或 undefined 不是有效的树结构数据
743
+ console.log(t.isSingleTreeData(null)) // false
744
+ console.log(t.isSingleTreeData(undefined)) // false
745
+
746
+ // children 不能是 null
747
+ const invalidTree = { id: 1, children: null }
748
+ console.log(t.isSingleTreeData(invalidTree)) // false
749
+
750
+ // 支持自定义字段名
751
+ const customTree = {
752
+ nodeId: 1,
753
+ name: 'node1',
754
+ subNodes: [{ nodeId: 2, name: 'node2' }],
755
+ };
756
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
757
+ console.log(t.isSingleTreeData(customTree, fieldNames)) // true
758
+ ```
759
+
760
+ ### isValidTreeNode(检查单个节点是否是有效的树节点结构)
761
+
762
+ 检查单个节点是否是有效的树节点结构(轻量级,不递归检查子节点)。只检查节点本身的结构,不检查子节点。
763
+
764
+ ```javascript
765
+ // 有效的树节点(有 children 数组)
766
+ const node1 = {
767
+ id: 1,
768
+ name: 'node1',
769
+ children: [{ id: 2 }],
770
+ };
771
+ console.log(t.isValidTreeNode(node1)) // true
772
+
773
+ // 有效的树节点(没有 children 字段)
774
+ const node2 = { id: 1, name: 'node1' };
775
+ console.log(t.isValidTreeNode(node2)) // true
776
+
777
+ // 无效的树节点(children 不是数组)
778
+ const invalidNode = {
779
+ id: 1,
780
+ children: 'not an array',
781
+ };
782
+ console.log(t.isValidTreeNode(invalidNode)) // false
783
+
784
+ // 支持自定义字段名
785
+ const customNode = {
786
+ nodeId: 1,
787
+ subNodes: [{ nodeId: 2 }],
788
+ };
789
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
790
+ console.log(t.isValidTreeNode(customNode, fieldNames)) // true
791
+ ```
792
+
793
+ **与 isSingleTreeData 的区别:**
794
+ - `isValidTreeNode` - 只检查单个节点的基本结构,不递归检查子节点(轻量级)
795
+ - `isSingleTreeData` - 递归检查整个树结构,确保所有子节点都是有效的树结构
796
+
797
+ ### isTreeNodeWithCircularCheck(检查节点结构并检测循环引用)
798
+
799
+ 检查节点是否是有效的树节点结构,并检测循环引用。使用 WeakSet 跟踪已访问的节点,如果发现循环引用则返回 false。
800
+
801
+ ```javascript
802
+ // 有效的树节点(无循环引用)
803
+ const validNode = {
804
+ id: 1,
805
+ children: [
806
+ { id: 2, children: [{ id: 3 }] },
807
+ ],
808
+ };
809
+ console.log(t.isTreeNodeWithCircularCheck(validNode)) // true
810
+
811
+ // 检测循环引用
812
+ const node1 = { id: 1, children: [] };
813
+ const node2 = { id: 2, children: [] };
814
+ node1.children.push(node2);
815
+ node2.children.push(node1); // 循环引用
816
+ console.log(t.isTreeNodeWithCircularCheck(node1)) // false
817
+
818
+ // 检测自引用
819
+ const selfRefNode = { id: 1, children: [] };
820
+ selfRefNode.children.push(selfRefNode); // 自引用
821
+ console.log(t.isTreeNodeWithCircularCheck(selfRefNode)) // false
822
+
823
+ // 支持自定义字段名
824
+ const customNode = {
825
+ nodeId: 1,
826
+ subNodes: [{ nodeId: 2 }],
827
+ };
828
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
829
+ console.log(t.isTreeNodeWithCircularCheck(customNode, fieldNames)) // true
830
+ ```
831
+
832
+ **使用场景:**
833
+ - 在接收用户输入或外部数据时,检查是否有循环引用
834
+ - 数据验证,防止无限递归
835
+ - 调试时检查数据结构是否正确
836
+
837
+ ### isSafeTreeDepth(检查树深度是否安全)
838
+
839
+ 检查树结构数据的深度是否安全(防止递归爆栈)。如果树的深度超过 `maxDepth`,返回 false。
840
+
841
+ ```javascript
842
+ // 深度安全的树
843
+ const safeTree = [
844
+ {
845
+ id: 1,
846
+ children: [
847
+ { id: 2, children: [{ id: 3 }] },
848
+ ],
849
+ },
850
+ ];
851
+ console.log(t.isSafeTreeDepth(safeTree, 10)) // true(深度为3,小于10)
852
+
853
+ // 深度超过最大深度
854
+ const deepTree = [
855
+ {
856
+ id: 1,
857
+ children: [
858
+ { id: 2, children: [{ id: 3 }] },
369
859
  ],
370
860
  },
371
861
  ];
862
+ console.log(t.isSafeTreeDepth(deepTree, 2)) // false(深度为3,超过2)
863
+
864
+ // 空树总是安全的
865
+ console.log(t.isSafeTreeDepth([], 10)) // true
866
+
867
+ // 单层树
868
+ const singleLayer = [{ id: 1 }, { id: 2 }];
869
+ console.log(t.isSafeTreeDepth(singleLayer, 1)) // true
372
870
 
871
+ // 支持自定义字段名
872
+ const customTree = [
873
+ {
874
+ nodeId: 1,
875
+ subNodes: [
876
+ { nodeId: 2, subNodes: [{ nodeId: 3 }] },
877
+ ],
878
+ },
879
+ ];
373
880
  const fieldNames = { children: 'subNodes', id: 'nodeId' };
374
- const isValidCustom = t.isMultipleTrees(customForest, fieldNames)
375
- console.log(isValidCustom) // true
881
+ console.log(t.isSafeTreeDepth(customTree, 3, fieldNames)) // true
882
+ console.log(t.isSafeTreeDepth(customTree, 2, fieldNames)) // false
376
883
  ```
377
884
 
885
+ **使用场景:**
886
+ - 在处理大型树之前,先检查深度是否安全
887
+ - 防止递归调用栈溢出
888
+ - 性能优化,避免处理过深的树结构
889
+
378
890
  ## 自定义字段名
379
891
 
380
892
  所有方法都支持自定义 children 和 id 的属性名,通过最后一个参数传入配置对象:
381
893
 
382
894
  ```javascript
895
+ // 使用默认字段名
896
+ const foundNode1 = t.findTree(treeData, (node) => node.id === 2)
897
+
898
+ // 使用自定义字段名
383
899
  const fieldNames = { children: 'subNodes', id: 'nodeId' };
384
- const result = t.findTree(treeData, (item) => item.nodeId === 2, fieldNames);
900
+ const foundNode2 = t.findTree(customTreeData, (node) => node.nodeId === 2, fieldNames);
385
901
  ```
386
902
 
903
+ **注意:** 所有 30 个函数都支持 `fieldNames` 参数,保持 API 一致性。即使某些函数(如 `isEmptyTreeData`)中该参数不生效,也可以传入以保持代码风格一致。
904
+
387
905
  ## 测试
388
906
 
389
907
  ### 运行测试