前端快速生成 parent_id 關聯的樹結構數據

来源:https://www.cnblogs.com/eurkidu/archive/2020/07/21/qian-duan-kuai-su-sheng-cheng-parentid-tree.html
-Advertisement-
Play Games

如果想直接看結論,可直接跳轉到 快速生成方法在ToB的項目裡面,總免不了處理樹相關的數據,比如多級部門列表,比如多級分銷列表等等,凡是有上下級關聯的級聯數據,總免不了生成樹結構的數據。樹結構的數據在後端有很多種存儲方法,最常見的就是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 優化的話可以讓用戶在獲得流暢的訪問體驗上,快速看到所需的數據。

才疏學淺。如有疏漏歡迎指正,有建議敬請提出。歡迎各位大佬一起討論。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目錄結構 目錄名作用 bin 存放系統腳本 conf 存放配置文件 contrib zk附加功能支持 dist-maven maven倉庫文件 docs zk文檔 lib 依賴的第三方庫 recipes 經典場景樣例代碼 src zk源碼 conf 目錄 conf 目錄用來存檔配置文件,zoo.cf ...
  • kafka的配置分為 broker、producter、consumer三個不同的配置 一 、BROKER 的全局配置 最為核心的三個配置 broker.id、log.dir、zookeeper.connect 。 系統 相關 ##每一個broker在集群中的唯一標示,要求是正數。在改變IP地址,不 ...
  • 資料庫索引,相信大家都不陌生吧。 索引是對資料庫表中一列或多列的值進行排序的一種結構,使用索引可快速訪問資料庫表中的特定信息。作為輔助查詢的工具,合理的設計索引能很大程度上減輕db的查詢壓力,db我們都知道,是項目最核心也是最薄弱的地方,如果壓力太大很容易產生故障,造成難以預計的影響。所以,不管是日 ...
  • 太累了,不想來回覆制粘貼,多麼想有一鍵發佈到各大寫作平臺上的功能。 說重點,Golang學習系列第五天: Golang和PostgreSQL開發 RESTful API:https://blog.csdn.net/dong19891210/article/details/107424704 ...
  • 7 種數據類型分為簡單和複雜兩種 簡單數據類型: number, string, boolean, null, undefined, symbol 簡單數據類型存儲方式:直接存放在Stack(棧)里 註意:string可以存在棧也可存在堆 複雜數據類型:object(包括array和function ...
  • 隨著技術的發展,移動設備越來越流行,並且不同設備間屏幕尺寸和屏幕像素的差異,移動端開發麵臨著多解析度適配的問題。 ...
  • 關於函數name屬性,不同聲明方式有所差異,沒有規律只能硬記 ...
  • 1.字體屬性 color,規定文本的顏色,如 div{color:red;} font-style,規定文本顯示方式,如 p.normal {font-style: normal;} ,有normal(正常顯示)、italic(斜體顯示,字體結構有一定變化)、oblique(傾斜顯示,僅僅是文本的傾 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...