treeToList【树型转列表】🔥🔥🔥
描述
将 tree(树状)结构转化成 平面一层的 list 常用
# 1.示例
import { treeToList } from 'sf-utils2'
const tree = [
{
id: 1,
name: '香蕉',
pId: null,
children: [
{
id: 1001,
name: '香蕉A',
pId: 1,
children: [
{
id: 1001001,
name: '香蕉A-儿子',
pId: 1001,
children: [
{
id: 1001001001,
name: '香蕉A-儿子-儿子',
pId: 1001001
}
]
}
]
},
{
id: 1002,
name: '香蕉B',
pId: 1,
children: [
{
id: 1002002,
name: '香蕉B-儿子',
pId: 1002
}
]
},
{
id: 1003,
name: '香蕉C',
pId: 1,
children: [
{
id: 1003003,
name: '香蕉C-儿子',
pId: 1003
}
]
}
]
},
{
id: 2,
name: '苹果',
pId: null,
children: [
{
id: 1004,
name: '苹果A',
pId: 2,
children: [
{
id: 1004004,
name: '苹果A-儿子',
pId: 1004
}
]
},
{
id: 1007,
name: '苹果D',
pId: 2
},
{
id: 1008,
name: '苹果D',
pId: 2
},
{
id: 1009,
name: '苹果C',
pId: 2
}
]
},
{
id: 3,
name: '橘子',
pId: null,
children: [
{
id: 1005,
name: '橘子C',
pId: 3,
children: [
{
id: 1005005,
name: '橘子C-儿子',
pId: 1005
}
]
},
{
id: 1006,
name: '橘子B',
pId: 3,
children: [
{
id: 1006006,
name: '橘子B-儿子',
pId: 1006
}
]
},
{
id: 1010,
name: '橘子A',
pId: 3
}
]
},
{
id: 4,
name: '西瓜',
pId: null,
children: [
{
id: 1012,
name: '西瓜B',
pId: 4,
children: [
{
id: 1013,
name: '西瓜B-儿子',
pId: 1012
}
]
}
]
},
{
id: 1011,
name: '西瓜A',
pId: null
}
]
// tree转化成list, 对应下面输出结果1
treeToList({ tree: tree, props: { children: 'children' } })
// tree转化成list,对应下面输出结果2
// 保留每一项节点 所有的子代以及所有子代下所有节点(平铺化)、保留每一项节点所经过的路径节点,字段为_pathNodes
treeToList({
tree: tree,
props: { children: 'children' },
retainParent: true,
retainPaths: true,
retainAllChildren: true
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
⚠️ 注意
这边在 tree 转成 list 的时候,treeToList 方法会默认给每个节点加上 __id__ 与 __pId__,__id__ 每一个子节点唯一值,__pId__ 每一个子节点中的父节点值,
你会发现这边的 __id__ 看起来是有规则的,它是下标索引为基值,比如 __id__ 是 0-1-1-1-1 时,表面它处于第 4 层、之前途径的路径是 0-1、0-1-1、
0-1-1-1, 这样方便平时业务中做其他操作,比如要获取每个子节点所有途经的节点集合,我们只需要遍历刚才的 0-1、0-1-1、
0-1-1-1 去 list 中找到 __id__ 是其中之一。是不是感觉很方便?😁
v3.3.0+ 每一个节点都会增加 __prevNode__(当前节点相邻的上一个节点)属性、__nextNode__(当前节点相邻的下一个节点)属性。
// treeToList 为每个节点增加属性如下,__depth__、__id__、__level__、__pId__、__rootNode__、__pathNodes__、__allChildren__、__rootNode__
// children: (3) [{…}, {…}, {…}]
// id: 1
// name: "香蕉"
// order: 2
// parentId: null
// __depth__: 1 // 当前节点深度
// __id__: "0-1" // 节点经过的路径
// __level__: 1 // 当前节点深度
// __pId__: "0" // 父节点经过的路径
// __pathNodes__: [{…}, {…}, {…}], // 节点经过的nodeList 由属性 retainPaths 控制
// __parentNode__: {id: 1, name: '香蕉', parentId: null, order: 2, children: Array(3), …} // 当前父节点
// __allChildren__: [{…}, {…}, {…}], // 该节点下所有子节点 由属性 retainAllChildren 控制
// __rootNode__: {id: 1, name: '香蕉', parentId: null, order: 2, children: Array(3), …} // 顶层根节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出结果,点击查看
;[
{ id: 1, name: '香蕉', pId: null, __pId__: '0', __id__: '0-1' },
{ id: 1001, name: '香蕉A', pId: 1, __pId__: '0-1', __id__: '0-1-1' },
{ id: 1001001, name: '香蕉A-儿子', pId: 1001, __pId__: '0-1-1', __id__: '0-1-1-1' },
{ id: 1001001001, name: '香蕉A-儿子-儿子', pId: 1001001, __pId__: '0-1-1-1', __id__: '0-1-1-1-1' },
{ id: 1002, name: '香蕉B', pId: 1, __pId__: '0-1', __id__: '0-1-2' },
{ id: 1002002, name: '香蕉B-儿子', pId: 1002, __pId__: '0-1-2', __id__: '0-1-2-1' },
{ id: 1003, name: '香蕉C', pId: 1, __pId__: '0-1', __id__: '0-1-3' },
{ id: 1003003, name: '香蕉C-儿子', pId: 1003, __pId__: '0-1-3', __id__: '0-1-3-1' },
{ id: 2, name: '苹果', pId: null, __pId__: '0', __id__: '0-2' },
{ id: 1004, name: '苹果A', pId: 2, __pId__: '0-2', __id__: '0-2-1' },
{ id: 1004004, name: '苹果A-儿子', pId: 1004, __pId__: '0-2-1', __id__: '0-2-1-1' },
{ id: 1007, name: '苹果D', pId: 2, PD: '0-2-2' },
{ id: 1008, name: '苹果D', pId: 2, __pId__: '0-2', __id__: '0-2-3' },
{ id: 1009, name: '苹果C', pId: 2, __pId__: '0-2', __id__: '0-2-4' },
{ id: 3, name: '橘子', pId: null, __pId__: '0', __id__: '0-3' },
{ id: 1005, name: '橘子C', pId: 3, __pId__: '0-3', __id__: '0-3-1' },
{ id: 1005005, name: '橘子C-儿子', pId: 1005, __pId__: '0-3-1', __id__: '0-3-1-1' },
{ id: 1006, name: '橘子B', pId: 3, __pId__: '0-3', __id__: '0-3-2' },
{ id: 1006006, name: '橘子B-儿子', pId: 1006, __pId__: '0-3-2', __id__: '0-3-2-1' },
{ id: 1010, name: '橘子A', pId: 3, __pId__: '0-3', __id__: '0-3-3' },
{ id: 4, name: '西瓜', pId: null, __pId__: '0', __id__: '0-4' },
{ id: 1012, name: '西瓜B', pId: 4, __pId__: '0-4', __id__: '0-4-1' },
{ id: 1013, name: '西瓜B-儿子', pId: 1012, __pId__: '0-4-1', __id__: '0-4-1-1' },
{ id: 1011, name: '西瓜A', pId: null, __pId__: '0', __id__: '0-5' }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输出结果2
# 2.入参说明
# 主入参
| 参数 | 说明 | 类型 | 是否必填 | 默认值 |
|---|---|---|---|---|
| tree | 原数据(要转换的 tree 数据集) | Array | 是 | |
| props | 自定义字段属性名,详情见下 👇 | Object | 否 | |
| retainParent | 是否保留每一项中的 父节点,__parentNode__ | Boolean | 否 | false |
| retainChild | 是否保留每一项中的 子节点,children | Boolean | 否 | false |
| retainPaths | 是否返回每一项节点所经过的路径节点,字段为__pathNodes__ | Boolean | 否 | false |
| retainAllChildren | 是否返回每一项节点 所有的子代以及所有子代下所有节点(平铺化),字段为__allChildren__ | Boolean | 否 | false |
| isDeepClone | 是否要深拷贝原 tree 对象,默认true | Boolean | 否 | true |
# props 对象
| 参数 | 说明 | 类型 | 是否必填 | 默认值 |
|---|---|---|---|---|
| children | 定义 children 键名 | String | 否 | children |
# 3.源码
import isArray from '@/base/isArray'
import merge from '../object/merge'
import def from '@/object/def'
import deepClone from '@/object/deepClone'
import { _includesChildPath } from '@/_helper/_helperTreeBase'
/**
* 获取节点 经过的所有父节点 轨迹节点
* @param {Object} listObj
* @param {string|number} ID
* @returns {any[]}
*/
function _getNodePathItem(listObj = {}, ID = '') {
const ids = ID.toString().split('-')
let paths = []
for (let i = 1; i <= ids.length; i++) {
paths.push(ids.slice(0, i).join('-'))
}
paths.shift()
paths.pop()
return paths
.map(v => {
return listObj[v]
})
.filter(Boolean)
}
/**
* 将tree结构 转化成 list
* @param tree 要转换的tree数据集
* @param {{children?: 'children' }} props 自定义字段
* @param retainChild 是否保留每一项中的 直接子节点
* @param retainPaths 是否返回每一项节点所经过的路径节点
* @param retainAllChildren 是否返回每一项节点 所有的子代以及所有子代下所有节点(平铺化)
* @param isDeepClone 是否深度克隆原树型对象
* @param retainParent
* @returns {*[]}
*/
// Object.defineProperty(item, '_id', { writable: false, value: `${item._pId}-${index + 1}` })
function treeToList({
tree = [],
props = { children: 'children' },
retainChild = false,
retainPaths = false,
retainAllChildren = false,
isDeepClone = true,
retainParent = false
}) {
let defaultProps = { children: 'children' }
props = merge({}, defaultProps, props)
if (isDeepClone) tree = deepClone(tree)
const _list = []
const _PID = '@'
let _listObj = {}
const treeToListFn = ({ tree = [], props = { children: 'children' }, retainChild = false, PID = '@' }) => {
if (isArray(tree) && tree.length) {
tree.forEach((item, index) => {
let _pId = item.__pId__ ?? PID
def(item, '__pId__', _pId)
def(item, '__id__', `${_pId}-${index}`)
_list.push(item)
if (isArray(item[props.children]) && item[props.children].length) {
treeToListFn({
tree: item[props.children],
props,
retainChild,
PID: item.__id__
})
}
})
}
}
treeToListFn({ tree, props, retainChild, PID: _PID })
if (retainPaths || retainAllChildren || retainParent) {
_listObj = _list.reduce((pre, cur) => {
if (cur) {
pre[cur.__id__] = cur
}
return pre
}, {})
}
_list.forEach(v => {
// 是否保留每一项中的 直接子节点
if (!retainPaths) {
delete v[props.children]
}
// 是否返回每一项节点 所有的子代以及所有子代下所有节点(平铺化)
if (retainAllChildren) {
def(
v,
'__allChildren__',
_list.filter(o => {
if (_includesChildPath(o.__id__, v.__id__)) {
return true
}
// const idx = String(o.__pId__).indexOf(v.__id__)
// const hasChild = !!o[props.children]?.length // 是否含有子节点
// if (!hasChild && idx == 0 && String(o.__id__).substr(v.__id__.length, 1) == '-') {
// return true
// }
})
)
}
// 是否保留每一项中的 直接子节点
if (retainPaths) {
def(v, '__rootNode__', _listObj[v.__id__.split('-').slice(0, 2).join('-')] || v)
def(v, '__pathNodes__', _getNodePathItem(_listObj, v.__id__))
}
def(v, '__level__', String(v.__pId__).split('-').length) // 当前节点处于深度(即第几层)
if (retainParent) {
def(v, '__parentNode__', _listObj[v.__pId__])
}
})
return _list
}
export default treeToList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
上次更新: 2025/01/11, 15:37:45