1

背景

因为项目缘故,需要实现一个数组转树形结构的数据,主要是用于树组件的渲染。一开始没有去网络上找相关的成熟的算法,自己思考自己写了一个,可以用,也用了一段时间。但是有一次,数据量特别大,数据又有些特别(非特殊的树形结构,子节点判断一部分是通过 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+, 提升的还是很直观的。

我想应该是有更好的方法,如果你有更好的算法贴出来一起分享下?

其他

JS数组reduce()方法详解及高级技巧


拳头巴掌
120 声望0 粉丝