前端tree組件,10000個樹節點,從14.65s到0.49s
全篇主要思想
遞歸的本質是棧的讀取
先看效果對比:
以下都是基於10000條子節點數據作對比,先上最終數據對比:
遞歸版tree,渲染速度: 14.65s,點擊節點處理速度: 9.83s
優化版tree,渲染速度: 0.49s,點擊節點處理速度: 0.18s
遞歸組件實現tree:渲染速度 15.71s -1.06s = 14.65s
遞歸組件版tree點擊節點性能分析圖:點擊節點處理速度: 10.19s - 0.357s = 9.833s ≈ 9.83s
最終優化實現tree: 渲染速度2.25s - 1.76s = 0.49s
優化版tree點擊節點性能分析圖:點擊節點處理速度0.623s - 0.3s = 0.3s
最終對比是
遞歸版tree,渲染速度: 12.19s,點擊節點處理速度: 9.52s
優化版tree,渲染速度: 0.49s,點擊節點處理速度: 0.18s
分析問題
我們可以藉助performance分析一下遞歸組件的耗時點所在,上遞歸組件渲染的性能分析:
1、script耗時分析:
通過圖-1性能瀑布可以清晰的看到script執行佔了8.9s的時間,通過上圖即圖-5可以看到script的的調用棧主要集中在創建vue實例時的createChildren上面。
2、render耗時分析:
通過上圖即圖-6可以清晰的看到render耗時主要集中在Recalculate Style、Layout上面。我們知道Recalculate Style、Layout主要是樣式計算,因此查看代碼:
發現在遞歸的tree-node組件裡面有很多樣式的計算,10000條子節點就需要計算10000次。
3、DOM結構分析:
由上圖即圖-8我們可以看出遞歸組件生成的DOM結構也相應是嵌套的結構,因此DOM不僅有縱向的結構也同時有嵌套層次的結構,因此這時控制DOM的數量很難。
由上面的script、render、DOM結構的分析,可以看出問題癥結所在:創建vue實例過多!
因此我們如果要優化,那麼只要減少vue實例的創建,那麼問題就得到了解決,因此接下來來看是如何實現的。
實現思想
來看我們的開篇思想:
遞歸的本質是棧的讀取
在演算法中我們會遇到很多遞歸實現的案例,所有的遞歸都可以轉換成非遞歸實現,其中轉換的本質是:遞歸是解析器(引擎)來幫我們做了棧的存取,非遞歸是手動創建棧來模擬棧的存取過程。
萬物都是相通的,遞歸組件也可以轉換成扁平數組來實現:
1、更改DOM結構成平級結構,點擊節點以及節點的視覺樣式通過操作總的list數據去實現
2、然後使用虛擬長列表來控制vue子組件實例創建的數量。
優化版實現
主要分為兩部分功能:
1、tree數據和DOM結構的扁平化
2、虛擬長列表控制DOM渲染數量
1、tree數據和DOM結構的扁平化
由上圖我們可以看到經過改造之後的tree的DOM結構,父節點和子節點是平級的,在操作子節點時去操作內存中的listData數據來改變相關聯節點的狀態。
我們再看下listData數據的結構,:
上圖即圖-10結合圖-9的DOM結構可以對整個功能的實現邏輯一目了然:
listData中的每一項的style、checked、path等信息來描述節點的樣式位置和狀態,操作一個節點時通過listData更改相關節點的狀態樣式等信息。
因此最終來寫我們的代碼:
我們再看下handleCheckChange的做了什麼:
handleCheckChange 處理了父節點和子節點的check和半選狀態,一切都是操作的listData中的數據。
現在我們是把所有listData都通過循環渲染出來,其實我們可以只渲染可視區的節點數據。
listData數據的扁平化及DOM的扁平化為接下來我們接入虛擬長列表的功能提供了可能。
2、虛擬長列表控制DOM渲染數量
實現思路:
根節點DOM分成兩個子節點:fui-tree__phantom 和 fui-tree__content
兩個子節點都是絕對定位,為了在滾動時避免數據的更改回頭觸發滾動事件
根節點解兩個子節點css:
.fui-tree {
height: 400px;
overflow: auto;
position: relative;
}
.fui-tree__phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.fui-tree__content {
left: 0;
right: 0;
top: 0;
position: absolute;
}
然後我們通過滾動條的位置來計算我們應該要取哪些數據。
主要代碼:
通過startIndex、endIndex可以取出我們需要循環的數據列表renderNodes:
computed: {
renderNodes() {
if (!this.treeNode) return []
return this.treeNode.listData.slice(this.positionConfig.startIndex, this.positionConfig.endIndex)
},
phantomStyle() {
return {
height: this.allListLen * 20 + px
}
}
}
結合圖-11的v-for,這樣我們在渲染時的dom數量是固定的條數,如下圖:
虛擬列表的接入可以讓即使再多數據量也能渲染固定的DOM數量,這樣就可以支撐更大數據的渲染和功能。
以上我們實現了業務需求的大數據渲染,目前測試可支撐到20w條節點,點擊子節點時會有肉眼可見的延遲,主要是圖-12中handleCheckChange的數據查找和處理,這塊還有一定的優化空間:使用字典樹存儲節點相關信息,字典樹和扁平數組listData的每一個元素指向同一個內存地址,在handleCheckChange中通過操作字典樹來達到操作listData的元素的效果,經典的空間換時間的案例。
前端tree組件,10000個樹節點,從12.19s到0.49s - 掘金參考:
Furybean:再談前端虛擬列表的實現開源 UI 庫中,唯一同時實現了大表格虛擬化和樹表格的 Table 組件
推薦閱讀: