前端快速生成 parent_id 关联的树结构数据

如果想直接看结论,可直接跳转到 快速生成方法

在ToB的项目里面,总免不了处理树相关的数据,比如多级部门列表,比如多级分销列表等等,凡是有上下级关联的级联数据,总免不了生成树结构的数据。

树结构的数据在后端有很多种存储方法,最常见的就是parent_id这种上下级关联表。当然还有其它的例如左右值树表等存储结构。

本文暂时只讨论parent_id关联的这种树结构,这时候后端范围的数据为所有树节点的list数据。

准备-测试数据生成代码

为了方便测试,先写一段随机生成parent_id关联list数据的代码

代码

// genTestData.js
function getRandomByRange(start, end) {
  let length = end - start
  return start + (Math.random() * length) >>> 0
}

function getRandomCode() {
  return Math.random().toString(36).slice(2)
}

function getRandomCodeByData(data) {
  return data[getRandomByRange(0, data.length)].code
}

export default function() {
  let data = []
  // 随机生成 20-30 个一级节点
  for (let i = 0, l = getRandomByRange(20, 30); i < l; i++) {
    data.push({
      facode: '0',
      code: getRandomCode()
    })
  }
  // 随机生成 100-120 个随机关联节点
  for (let i = 0, l = getRandomByRange(100, 120); i < l; i++) {
    data.push({
      facode: getRandomCodeByData(data),
      code: getRandomCode()
    })
  }
  return data.sort(() => (Math.random() - 0.5))
}

可修改 getRandomByRange 的参数,生成不同数量的测试数据

测试

测试生成效果如下

// demo.js
import genTestData from './genTestData.js'
const oriData = genTestData()
console.table(oriData)

这时候有了测试数据,现在开始进入正题

常规生成方法

思路

从根节点开始,遍历数据源,每次找到当前节点的子节点,挂到当前节点的 children 上,然后递归调用

代码

// tree.js
export class TreeCreate {
  constructor(treeNode, treeConfig) {
    this.treeNode = treeNode
    this.treeConfig = Object.assign({
      fId: 'pid', // 关联的 parent_id 字段名
      id: 'id', // 关联的 parent_id 的 id 字段名
      rootId: '0', // 开始的 根节点关联的 parent_id 对应的 字段名
    }, treeConfig)
    this.treeData = [] // 树数据
  }
  // 树生成, 递归调用
  createTree(data, parent = null) {
    let config = this.treeConfig
    let TreeNode = this.treeNode
    let treeData, pid, i, l, val, children
    treeData = []
    for (i = 0, l = data.length; i < l; i++) {
      val = data[i]
      if (parent === null) {
        pid = config.rootId
      } else {
        pid = parent[config.id]
      }
      if (val[config.fId] === pid) {
        let t = {}
        TreeNode.call(t, val)
        children = this.createTree(data, Object.assign(t, val))
        treeData.push(t)
        treeData[treeData.length - 1].children = children
      }
    }
    return treeData
  }
  // 生成树
  create(data) {
    this.treeData = this.createTree(data)
  }
  // 获取树状的树数据(深拷贝)
  getTreeData() {
    // 演示简易处理,无法处理循环引用对象,如果树节点需要保留父级引用,则需要改为可以处理循环引用的深拷贝方法
    return JSON.parse(JSON.stringify(this.treeData))
  }
}

使用方式

treeNode 传入树节点数据生成函数,不能使用箭头函数,this为当前节点,默认空对象。 接受参数item为原始数据对象,根据需要为this对象赋值字段。如果简单需要全字段的,可以通过Object.keys遍历参数的所有字段,然后全部赋值到this的对应字段上。
treeConfig 为配置参数

  • FId 关联的 parent_id 字段名
  • Id 关联的 parent_id 的 id 字段名
  • rootId 开始的 根节点关联的 parent_id 对应的 字段名

测试性能

import genTestData from './genTestData.js'
import { TreeCreate } from './tree.js'
// 测试数据
const oriData = genTestData()
console.log(`树节点数量 ${oriData.length}`)

// 树
let demoTree = new TreeCreate(function(item) {
  for (let key of Object.keys(item)) {
    this[key] = item[key]
  }
}, { fId: 'facode', id: 'code', rootId: '0' })

console.time('生成树时间')
demoTree.create(oriData, true)
console.timeEnd('生成树时间')

让我们测试下性能

百级数据

千级数据

可以看到性能已经有了明显的下降

万级数据

耗时将近3s多,会明显造成页面卡顿

这时候我们来找下原因,为什么会让生成函数随着n的增加下降这么严重。
如果仔细想想刚才的算法,不难发现其实其时间复杂度基本为O(n²),会随着n的增加快速增加。

如果数据量改为十万级,基本处于页面卡死的状态,当然正常情况下不会出现前端需要处理十万级的数据,肯定是后端偷懒了,不过正常万级的数据还是有可能需要处理的。

如何优化

仔细思考代码重复跑的地方,发现每次找某个节点的children的时候,会遍历全部的数据源去判断是否是当前节点的children。

这时候第一反应的优化点就是,当A节点已经是B节点的children的时候,就从数据源中删除A节点,下次去查找C节点的children的时候就不会遍历到A节点了。

这个可以自己尝试下,有效果,不明显,因为没有改变某个节点被多次遍历的本质,例如A节点是最后一个节点的children,那么前面的children生成,还是每次会遍历到A节点,A节点还是会被遍历到多次。
而且从数据源中移除某个节点,其实也是耗时操作。

综上所述,耗时是因为节点被无效的多次遍历造成的,那么能不能只遍历一次然后用空间换时间的常规操作来优化遍历呢。
这时候我们就会想到js的map对象,hash存储,通过key可以直接得到value。
如果用fId作为key,存储所有children信息作为value,那这样每次查找当前节点的children的时候就从遍历查找变成了从map中取值,因为hash存储的关系,基本是忽略不计的取值时间。

快速生成方法

思路

思路上面的优化里面已经说了,用js的map的key-value的特性,遍历一次查找好所有的父子关系,然后递归的地方只管生成就行了

代码

// tree.js
export class TreeCreateFast {
  constructor(treeNode, treeConfig) {
    this.treeNode = treeNode
    this.treeConfig = Object.assign({
      fId: 'pid', // 关联的 parent_id 字段名
      id: 'id', // 关联的 parent_id 的 id 字段名
      rootId: '0', // 开始的 根节点关联的 parent_id 对应的 字段名
    }, treeConfig)
    this.treeData = [] // 树状的树数据
  }
  // 按 fId 分组
  groupBy(data) {
    let config = this.treeConfig
    let group = []
    data.forEach(v => {
      let key = v[config.fId]
      if (!group.hasOwnProperty(key)) {
        group[key] = [v]
      } else {
        group[key].push(v)
      }
    })
    return group
  }
  // 树生成, 递归调用
  createTree(data, parent = null) {
    let config = this.treeConfig
    let TreeNode = this.treeNode
    let treeData, pid, children
    treeData = []
    if (parent === null) {
      pid = config.rootId
    } else {
      pid = parent[config.id]
    }
    if (data.hasOwnProperty(pid)) {
      data[pid].forEach(val => {
        let t = {}
        TreeNode.call(t, val)
        children = this.createTree(data, Object.assign(t, val))
        treeData.push(t)
        treeData[treeData.length - 1].children = children
      })
    }
    return treeData
  }
  // 生成树
  create(data) {
    this.treeData = this.createTree(this.groupBy(data))
  }
  // 获取树状的树数据(深拷贝)
  getTreeData() {
    // 演示简易处理,无法处理循环引用对象,如果树节点需要保留父级引用,则需要改为可以处理循环引用的深拷贝方法
    return JSON.parse(JSON.stringify(this.treeData))
  }
}

使用方式

使用方式跟常规的树生成函数一致

测试性能

import genTestData from './genTestData.js'
import { TreeCreateFast } from './tree.js'
// 测试数据
const oriData = genTestData()
console.log(`树节点数量 ${oriData.length}`)

// 树
let demoTree = new TreeCreateFast(function(item) {
  for (let key of Object.keys(item)) {
    this[key] = item[key]
  }
}, { fId: 'facode', id: 'code', rootId: '0' })

console.time('生成树时间')
demoTree.create(oriData, true)
console.timeEnd('生成树时间')

测试下性能,直接从万级开始

万级数据

在常规项目能接触的数据量上,已经处于无感了,基本就会卡个一帧

十万级数据

处于可以接受的范围,毕竟不可能有十万级的数据需要处理的

挑战下百万级

就玩玩试试,没任何实际意义

总结

通过map优化了遍历之后,解决多次重复的无用遍历之后,性能提升非常明显,基本常规项目使用场景下不会造成任何浏览器卡顿。
当然如果再使用 Web Worker 优化的话可以让用户在获得流畅的访问体验上,快速看到所需的数据。

才疏学浅。如有疏漏欢迎指正,有建议敬请提出。欢迎各位大佬一起讨论。

(0)

相关推荐

  • 再有人问你MySQL索引原理,就把这篇文章甩给他!

    作者 l zyz1992 来源 l Hollis(ID:hollischuang) 索引,可能让好很多人望而生畏,毕竟每次面试时候 MySQL 的索引一定是必问内容,哪怕先撇开面试,就在平常的开发中, ...

  • C/C++编程笔记:链接列表(链表)及其遍历,今天就教你

    像数组一样,链表是线性数据结构.与数组不同,链接列表元素不存储在连续的位置:元素使用指针链接. 为什么要链接列表? 数组可用于存储相似类型的线性数据,但是数组具有以下限制. 1)数组的大小是固定的:因 ...

  • 2021-02-24

    树形结构存储方案对比分析 在程序开发中,我们常遇到用树型结构来表示某些数据间的关系,如企业的组织架构.商品的分类.操作栏目等,目前的关系型数据库都是以二维表的形式记录存储数据,而树型结构的数据如需存入 ...

  • 菜鸟记207-两个小技巧:快速生成学期名称和复制公式文本

    关键词:EXCEL2016:连接符&:分列:操作难度*** 某天,小菜因为工作需要快速生成若干个学期名称,就像这样: 图 1:学期简称 怎么能快速生成这样的学期简称呢?小菜尝试用自动填充的方法 ...

  • 一种快速止痛和骨痂快速生成的药剂制作方法.pdf

    本发明涉及一种跌打损伤.冻结肩病人的快速止痛和骨折病人骨痂快速生成的药剂制作方法.     目前用于跌打损伤病人止痛.骨折病人促进骨痂生成的药物主要是西药,为止痛片或钙片.这些药物虽有一定疗效,但效果 ...

  • 制图小技巧丨巧用3D Map Generator快速生成地形神器

    又到了每周一的制图教室啦. 无论是建筑.规划还是景观专业,地形分析都会是我们经常用到的一类分析图.这类分析图的常规做法呢,就是先要获取地理数据,然后通过GIS.SU或者Rhino+GH生成场地模型,要 ...

  • 1102:Excel如何快速计算出红色单元格数据的和

    Excel如何快速计算出红色单元格数据的和

  • 快速汇总销量和,用数据透视表,比函数更简单

    快速汇总数据,很多伙伴可能想到的是用函数进行汇总求和,其实还可以用数据透视表,比函数简单,又实用多了.下面一起来看个例子. 如图是原数据和汇总后的结果,这是怎么做的呢? 图为原数据 图为汇总后的结果 ...

  • 1秒钟快速生成各种特效?这个Ps插件有点神奇!

    那么问题来了 究竟SUNNY欧巴给马工的OMP是啥呢? 别急,待会就送给你哈~ (谜底,等下揭晓,先看看下面的图) 有没有觉得特别有氛围,特别有感觉? 但要调出这种感觉 真的有点力不从心啊 而且还极为 ...

  • POWER QUERY--多张工作表的关联汇总-匹配数据

    送人玫瑰,手有余香,请将文章分享给更多朋友 动手操作是熟练掌握EXCEL的最快捷途径! 很多数据会依据其功能保存在不同的工作表,但它们之间是通过某个或某几个字段进行关联.例如,有两张表,一张是销售明细 ...

  • 快速生成短视频,快速文章转视频!3分钟教会你!

    互联网项目千千万,但是如果你没有流量的话,一切都是过眼云烟. 对于有流量的人来说,项目是变现工具. 对于没有流量的人来说,项目就是镰刀,而你就是韭菜. 如何搞流量? 简单来说,分成两种: 一种是付费流 ...

  • PPT入门:快速生成幻灯片的三种方法

    PPT是有很多张幻灯片组成的,如何自动生成幻灯片影响着我们制作PPT的效率,这里小编提供三种操作方法供大家选择,选择最适合自己的就好啦! 首先打开PPT. 根据上一节讲过的,新建空白文档之后,删除系统 ...