背景
因为项目缘故,需要实现一个数组转树形结构的数据,主要是用于树组件的渲染。一开始没有去网络上找相关的成熟的算法,自己思考自己写了一个,可以用,也用了一段时间。但是有一次,数据量特别大,数据又有些特别(非特殊的树形结构,子节点判断一部分是通过 parentId 判断,一部分又是通过其他节点判断),导致需要频繁调用数组转树的方法,结果就是出现性能问题。就 1000+ 的节点,结果数据处理用了 50+s 的时间。
经过一段时间调研和测试,做出以下总结。
数组转树的算法
方法 1:我自己的实现办法
/**
* 数组结构转换为树结构数据
* 注意: 数组里面的对象一定是排序过的,也就是说父级一定在前面,它的子级一定在后面
* @param arrayData 源数据
* @param parentId 父节点字段
* @param parentKey 父节点key值
* @param childrenKey 子节点的key值
* @param idKey id的key值
* @returns {Array}
*/
function arrayToTree1(arrayData, parentId = '', parentKey = 'pid', childrenKey = 'children', idKey = 'id') {
const treeObject = [] // 输出的结果
const remainderArrayData = [] // 遍历剩余的数组
arrayData = cloneDeep(arrayData)
arrayData.forEach(function (item) {
// 服务端返回的数据, parent没有的情况可能返回 null 值, 统一转换成 ''
if (item[parentKey] === null) {
item[parentKey] = ''
}
if (item[parentKey] === parentId) {
treeObject.push(item)
} else {
remainderArrayData.push(item)
}
})
treeObject.forEach(function (item) {
// 子节点遍历,性能问题也主要在这里,即使没有子节点,也会遍历一次
const _childrenData = arrayToTree(remainderArrayData, item[idKey], parentKey, childrenKey, idKey)
_childrenData && _childrenData.length > 0 && (item[childrenKey] = _childrenData)
})
return treeObject
}
方法 2:
/**
* 数组结构转换为树结构数据
* 注意: 数组里面的对象一定是排序过的,也就是说父级一定在前面,它的子级一定在后面
* @param arrayData 源数据
* @param parentId 父节点字段
* @param parentKey 父节点key值
* @param childrenKey 子节点的key值
* @param idKey id的key值
* @returns {Array}
*/
export function arrayToTree2(arrayData, parentId = '', parentKey = 'pid', childrenKey = 'children', idKey = 'id') {
const result = []
const _arrayData = cloneDeep(arrayData) // 深拷贝
_arrayData.forEach(item => {
// 服务端返回的数据, parent没有的情况可能返回 null 值, 统一转换成 ''
if (item[parentKey] === null) {
item[parentKey] = ''
}
if (item[parentKey] === parentId) {
result.push(item)
}
})
function recursionFn(treeData, arrayItem) {
treeData.forEach(item => {
// 找到父节点, 并添加到到 children 中. 如果当前节点不是目标节点的父节点, 就递归判断当前节点的子节点
if (item[idKey] === arrayItem[parentKey]) {
if (!item[childrenKey]) {
item[childrenKey] = []
}
item.children.push(arrayItem)
} else if (item[childrenKey] && item[childrenKey].length) {
recursionFn(item[childrenKey], arrayItem)
}
})
}
// 通过 reduce 遍历数组. 通过遍历每个元素去构建目标树数据
return _arrayData.reduce((previousValue, currentValue) => {
if (currentValue[parentKey]) { // 该节点是子节点
recursionFn(previousValue, currentValue)
}
return previousValue
}, result)
}
方法 3:
/**
* 数组结构转换为树结构数据
* @param arrayData 源数据
* @param parentId 父节点字段
* @param parentKey 父节点key值
* @param childrenKey 子节点的key值
* @param idKey id的key值
* @returns {Array}
*/
function arrayToTree3(array, parentId = '', parentKey = 'parentId', childrenKey = 'children', idKey = 'id') {
let arr = [...array]
let rootList = arr.filter((item) => {
// 服务端返回的数据, parent没有的情况可能返回 null 值, 统一转换成 ''
if (item[parentKey] === null) {
item[parentKey] = ''
}
if (item[parentKey] === parentId) {
return item
}
})
function listToTreeData(rootItem, arr, parentKey, idKey) {
rootItem.children = []
arr.forEach((item) => {
if (item[parentKey] === rootItem[idKey]) {
rootItem.children.push(item)
}
})
if (rootItem.children.length > 0) {
rootItem.children.forEach((item) => {
listToTreeData(item, arr, parentKey, idKey)
})
} else {
return false
}
}
rootList.map((rootItem) => {
listToTreeData(rootItem, arr, parentKey, idKey)
return rootItem
})
return rootList
}
测试
测试相关的代码
// 生成树数据的方法
function generateTreeData(size = 5, deep = 4) {
var result = []
var fn = function (parentId, size, level) {
for (var i = 0; i < size; i++) {
var _id = 'id_' + Math.floor(Math.random() * 10000000000)
result.push({
id: _id,
parentId: parentId
})
if (level < deep) {
fn(_id, size, level + 1)
}
}
}
fn('', size, 1)
return result
}
// 测试方法
function testMethod(fun, args, tag) {
args = cloneDeep(args)
console.time(tag)
var result = fun.apply(this, args)
console.log(result);
console.timeEnd(tag)
}
测试结果
var testData = generateTreeData(7, 5) // 共 19607 条数据, 每个节点下的子节点都是 7 个, 5 层的深度
// 正序(子节点必定在父节点后面)
testMethod(arrayToTree1, [testData, ''], 'arrayToTree1') // 14917.079833984375ms
testMethod(arrayToTree2, [testData, ''], 'arrayToTree2') // 3157.8740234375ms
testMethod(arrayToTree3, [testData, ''], 'arrayToTree3') // 5573.368896484375ms
// 乱序排序
testData = testData.sort(function randomSort() { return Math.random() > 0.5 ? -1 : 1 })
// 乱序
testMethod(arrayToTree1, [_arr, ''], 'arrayToTree1') // 22853.781982421875ms
testMethod(arrayToTree2, [_arr, ''], 'arrayToTree2') // 100 ms ~ 300 ms 之间, 看乱序的结果. 但结果是错的, 子节点有的有缺失
testMethod(arrayToTree3, [_arr, ''], 'arrayToTree3') // 11802.785888671875ms
从结果看,当然我自己写的算法效率慢的离谱,数据量小还好,数据量一大就灾难了。
方法 2,效率是很高,但是对数据有要求,必须是经过排序后的数据才有用,但实际业务场景中,树会有跨级排序的场景,所以很难保证是顺序的。
比较合适的方法是方法 3
我用方法 3 的算法,在我的项目中,时间从原来的 50+ 缩短到 5s+, 提升的还是很直观的。
我想应该是有更好的方法,如果你有更好的算法贴出来一起分享下?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。