全篇主要思想

遞歸的本質是棧的讀取

先看效果對比:

以下都是基於10000條子節點數據作對比,先上最終數據對比:

遞歸版tree,渲染速度: 14.65s,點擊節點處理速度: 9.83s

優化版tree,渲染速度: 0.49s,點擊節點處理速度: 0.18s

遞歸組件實現tree:渲染速度 15.71s -1.06s = 14.65s

遞歸版tree性能圖-1

遞歸組件版tree點擊節點性能分析圖:點擊節點處理速度: 10.19s - 0.357s = 9.833s ≈ 9.83s

遞歸tree點擊節點圖-2

最終優化實現tree: 渲染速度2.25s - 1.76s = 0.49s

優化tree性能圖-3

優化版tree點擊節點性能分析圖:點擊節點處理速度0.623s - 0.3s = 0.3s

優化tree點擊節點圖-4

最終對比是

遞歸版tree,渲染速度: 12.19s,點擊節點處理速度: 9.52s

優化版tree,渲染速度: 0.49s,點擊節點處理速度: 0.18s

分析問題

我們可以藉助performance分析一下遞歸組件的耗時點所在,上遞歸組件渲染的性能分析:

1、script耗時分析:

遞歸tree script性能分析圖-5

通過圖-1性能瀑布可以清晰的看到script執行佔了8.9s的時間,通過上圖即圖-5可以看到script的的調用棧主要集中在創建vue實例時的createChildren上面。

2、render耗時分析:

遞歸tree render性能分析圖-6

通過上圖即圖-6可以清晰的看到render耗時主要集中在Recalculate Style、Layout上面。我們知道Recalculate Style、Layout主要是樣式計算,因此查看代碼:

遞歸組件部分代碼圖-7

發現在遞歸的tree-node組件裡面有很多樣式的計算,10000條子節點就需要計算10000次。

3、DOM結構分析:

遞歸組件DOM結構圖-8

由上圖即圖-8我們可以看出遞歸組件生成的DOM結構也相應是嵌套的結構,因此DOM不僅有縱向的結構也同時有嵌套層次的結構,因此這時控制DOM的數量很難。

由上面的script、render、DOM結構的分析,可以看出問題癥結所在:創建vue實例過多!

因此我們如果要優化,那麼只要減少vue實例的創建,那麼問題就得到了解決,因此接下來來看是如何實現的。

實現思想

來看我們的開篇思想:

遞歸的本質是棧的讀取

在演算法中我們會遇到很多遞歸實現的案例,所有的遞歸都可以轉換成非遞歸實現,其中轉換的本質是:遞歸是解析器(引擎)來幫我們做了棧的存取,非遞歸是手動創建棧來模擬棧的存取過程

萬物都是相通的,遞歸組件也可以轉換成扁平數組來實現:

1、更改DOM結構成平級結構,點擊節點以及節點的視覺樣式通過操作總的list數據去實現

2、然後使用虛擬長列表來控制vue子組件實例創建的數量

優化版實現

主要分為兩部分功能:

1、tree數據和DOM結構的扁平化

2、虛擬長列表控制DOM渲染數量

1、tree數據和DOM結構的扁平化

優化版tree的DOM結構圖-9

由上圖我們可以看到經過改造之後的tree的DOM結構,父節點和子節點是平級的,在操作子節點時去操作內存中的listData數據來改變相關聯節點的狀態。

我們再看下listData數據的結構,:

優化版tree listData數據結構圖-10

上圖即圖-10結合圖-9的DOM結構可以對整個功能的實現邏輯一目了然:

listData中的每一項的style、checked、path等信息來描述節點的樣式位置和狀態,操作一個節點時通過listData更改相關節點的狀態樣式等信息。

因此最終來寫我們的代碼:

實現.vue代碼圖-11

我們再看下handleCheckChange的做了什麼:

handleCheckChange處理邏輯圖-12

handleCheckChange 處理了父節點和子節點的check和半選狀態,一切都是操作的listData中的數據。

現在我們是把所有listData都通過循環渲染出來,其實我們可以只渲染可視區的節點數據。

listData數據的扁平化及DOM的扁平化為接下來我們接入虛擬長列表的功能提供了可能。

2、虛擬長列表控制DOM渲染數量

實現思路:

根節點DOM分成兩個子節點:fui-tree__phantom 和 fui-tree__content

兩個子節點都是絕對定位,為了在滾動時避免數據的更改回頭觸發滾動事件

虛擬列表DOM組織結構圖-13

根節點解兩個子節點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;
}

然後我們通過滾動條的位置來計算我們應該要取哪些數據。

主要代碼:

計算可視區數據的起止索引index圖-14

通過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數量是固定的條數,如下圖:

優化版tree DOM數量是固定的圖-15

虛擬列表的接入可以讓即使再多數據量也能渲染固定的DOM數量,這樣就可以支撐更大數據的渲染和功能。


以上我們實現了業務需求的大數據渲染,目前測試可支撐到20w條節點,點擊子節點時會有肉眼可見的延遲,主要是圖-12中handleCheckChange的數據查找和處理,這塊還有一定的優化空間:使用字典樹存儲節點相關信息,字典樹和扁平數組listData的每一個元素指向同一個內存地址,在handleCheckChange中通過操作字典樹來達到操作listData的元素的效果,經典的空間換時間的案例。

前端tree組件,10000個樹節點,從12.19s到0.49s - 掘金?

juejin.im

參考:

Furybean:再談前端虛擬列表的實現?

zhuanlan.zhihu.com圖標開源 UI 庫中,唯一同時實現了大表格虛擬化和樹表格的 Table 組件?

juejin.im


推薦閱讀:

相关文章