Tree組件實(shí)現(xiàn)支持50W數(shù)據(jù)方法剖析
出師未捷身先死
有用戶在 fes-design VIP群 吐槽 Tree
組件在處理一萬條左右數(shù)據(jù)時(shí)很卡。但是 fes-design 已重視大數(shù)據(jù)場(chǎng)景,提供基礎(chǔ)的虛擬列表組件,以及選擇器、表格、樹形、級(jí)聯(lián)等組件基于虛擬列表處理了大數(shù)據(jù)場(chǎng)景,為啥 Tree
組件還卡呢?
Tree 自身的復(fù)雜性
Tree
數(shù)據(jù)結(jié)構(gòu)特性決定 Tree
組件中父子節(jié)點(diǎn)存在關(guān)聯(lián),以選中功能為例:
Select:選中只影響自身狀態(tài)。
Tree:當(dāng)開啟父子關(guān)聯(lián)時(shí),選中某個(gè)節(jié)點(diǎn)時(shí),其所有子孫節(jié)點(diǎn)全部選中,同時(shí)需計(jì)算父輩節(jié)點(diǎn)是否為全選中。
虛擬滾動(dòng)帶來的復(fù)雜性
虛擬滾動(dòng)是指根據(jù)滾動(dòng)距離計(jì)算當(dāng)前視野范圍需要展示的內(nèi)容。不管有多少數(shù)據(jù),只渲染視野范圍內(nèi)的選項(xiàng),大大減少了 Vue 實(shí)例的創(chuàng)建,性能無比優(yōu)越。因?yàn)樘摂M滾動(dòng)只接受一維數(shù)組結(jié)構(gòu),所以Tree
組件在初始化時(shí)需要把樹狀結(jié)構(gòu)數(shù)據(jù)按照展示順序拍平為一維數(shù)組。那么展開關(guān)閉的功能就變得復(fù)雜了!
不考慮虛擬滾動(dòng)方案時(shí)節(jié)點(diǎn)會(huì)這么設(shè)計(jì):
<div class="node"> <div>{{ node.label }}</div> <div v-show="node.expanded" v-for="child in node.children"> <Node node="child"/> </div> </div>
展開關(guān)閉只需要改變 node.expanded
。
考慮虛擬滾動(dòng)方案時(shí)節(jié)點(diǎn)會(huì)這么設(shè)計(jì):
<div class="node"> <div>{{ node.label }}</div> </div>
計(jì)算所有子孫節(jié)點(diǎn)狀態(tài),判斷節(jié)點(diǎn)是否顯示,如果顯示則把當(dāng)前節(jié)點(diǎn)丟到虛擬滾動(dòng)的一維數(shù)組中。
查問題
先用chrome的性能測(cè)試工具看看問題在哪:
可以找到耗時(shí)的代碼語句,下一步干掉他們。
怎么做
緩存數(shù)據(jù)
Tree
組件在初始化時(shí)會(huì)把樹狀結(jié)構(gòu)數(shù)據(jù)按照展示順序拍平為一維數(shù)組,在這個(gè)過程中,記錄每個(gè)節(jié)點(diǎn)的父級(jí)節(jié)點(diǎn)為indexPath 和所有子孫節(jié)點(diǎn)childrenPath。在后續(xù)邏輯中經(jīng)常會(huì)用到:
// 當(dāng)選中某個(gè)節(jié)點(diǎn)時(shí),只需要處理此節(jié)點(diǎn)相關(guān)上下節(jié)點(diǎn)狀態(tài) if (checkingNode) { const { indexPath } = checkingNode; indexPath.slice(0).reverse().forEach(computeIndeterminate); checkingNode.hasChildren && checkingNode.childrenPath.forEach( (key: TreeNodeKey) => { const node = nodeList.get(key); node.isIndeterminate.value = false; }, ); checkingNode = null; }
減少響應(yīng)式數(shù)據(jù)
在優(yōu)化前所有節(jié)點(diǎn)都會(huì)丟到nodeList中:
const nodeList = reactive<TreeNodeList>({}); // 轉(zhuǎn)換節(jié)點(diǎn)數(shù)據(jù) const copy = transformNode(node, indexPath, level); nodeList[copy.value] = copy;
數(shù)據(jù)量上來后,數(shù)據(jù)響應(yīng)式處理耗時(shí)非常大。所以我們不要把整個(gè)對(duì)象一股腦弄成響應(yīng)式的,只把需要的字段設(shè)置為響應(yīng)式的。
Tree
節(jié)點(diǎn)需要緩存的內(nèi)部狀態(tài)有是否開展、是否全選、是否選中,所以只需要這三個(gè)字段為響應(yīng)式:
const nodeList: Map<TreeNodeKey, InnerTreeOption> = new Map(); f (!nodeList.get(value)) { // Object.assign比解構(gòu)快很多 copy = Object.assign({}, newItem); copy.isExpanded = ref(false); copy.isIndeterminate = ref(false); copy.isChecked = ref(false); } nodeList.set(copy.value, copy);
用更快的 JS 語法
1、Array.concat 性能比較慢,改為使用賦值
export function concat(arr: any[], arr2: any[]) { const arrLength = arr.length; const arr2Length = arr2.length; arr.length = arrLength + arr2Length; for (let i = 0; i < arr2Length; i++) { arr[arrLength + i] = arr2[i]; } return arr; }
2、Map 的查找性能比 Object 稍好
const nodeList = {} ;
改為使用
const nodeList = new Map();
3、解構(gòu)語法比較慢,改為使用Object.assign
扣細(xì)節(jié)
1、computeCurrentData 是執(zhí)行非常耗時(shí)的函數(shù),由于 watch 兩個(gè)變量,在初始化時(shí)會(huì)執(zhí)行兩次,加上debounce只需要執(zhí)行一次。
watch( [currentExpandedKeys, transformData], debounce(() => { if (isSearchingRef.value) return; computeCurrentData(); }, 10), { immediate: true, }, );
2、葉子節(jié)點(diǎn)不需要計(jì)算isExpanded
if (node.hasChildren) { node.isExpanded.value = expandedKeys.includes(key); }
3、計(jì)算顯示的節(jié)點(diǎn)時(shí),可以先判斷是否由展開或者關(guān)閉節(jié)點(diǎn)觸發(fā)的計(jì)算,如果是則只需要計(jì)算此節(jié)點(diǎn)子孫和父級(jí)節(jié)點(diǎn)狀態(tài),而不需要計(jì)算全部節(jié)點(diǎn)
const computeCurrentData = ()=> { if(expandingNode) { // 計(jì)算此節(jié)點(diǎn)相關(guān)節(jié)點(diǎn) return } // 遍歷所有節(jié)點(diǎn) }
類似這種細(xì)節(jié)非常多,通過性能測(cè)試工具和自己經(jīng)驗(yàn)?zāi)苷业胶芏嗟胤?,積少成多,性能能提升不少。
數(shù)據(jù)結(jié)構(gòu)一致性的魅力
以收起節(jié)點(diǎn)為例:
常規(guī)思路是:當(dāng)點(diǎn)擊收起節(jié)點(diǎn)時(shí),判斷當(dāng)前所有子孫節(jié)點(diǎn)是否在顯示數(shù)據(jù)數(shù)組中,如果在就刪掉。復(fù)雜度是O(n^2)。
但是可以換個(gè)思路:由于childrenPath和currentData的順序一致,只需要遍歷一次childrenPath,判斷是是否為當(dāng)前節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn),如果是,刪掉就好。復(fù)雜度是O(n)
const deleteNode = (keys: TreeNodeKey[], index: number) => { let len = 0; keys.forEach((key) => { if (key === currentData.value[index + len]) { len += 1; } }); currentData.value.splice(index, len); }; const index = currentData.value.indexOf(expandingNode.value); deleteNode(expandingNode.childrenPath, index + 1);
在 Tree
的代碼中有很多地方,可以通過特殊的數(shù)據(jù)結(jié)構(gòu)來減少或者避免循環(huán),性能提升非常大!
歡迎來體驗(yàn): fes-design
以上就是Tree組件實(shí)現(xiàn)支持50W數(shù)據(jù)方法剖析的詳細(xì)內(nèi)容,更多關(guān)于Tree組件50W數(shù)據(jù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)TCP/IP協(xié)議的收發(fā)數(shù)據(jù)(服務(wù)端)代碼實(shí)例
這篇文章主要介紹了Java實(shí)現(xiàn)TCP/IP協(xié)議的收發(fā)數(shù)據(jù)(服務(wù)端)代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11Spring Boot中@ConditionalOnProperty的使用方法
這篇文章主要給大家介紹了關(guān)于Spring Boot中@ConditionalOnProperty的使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Java微信公眾平臺(tái)開發(fā)(7) 公眾平臺(tái)測(cè)試帳號(hào)的申請(qǐng)
這篇文章主要為大家詳細(xì)介紹了Java微信公眾平臺(tái)開發(fā)第七步,微信公眾平臺(tái)測(cè)試帳號(hào)的申請(qǐng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04MybatisPlus如何自定義TypeHandler映射JSON類型為L(zhǎng)ist
這篇文章主要介紹了MybatisPlus如何自定義TypeHandler映射JSON類型為L(zhǎng)ist,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Java格式化輸出詳細(xì)講解(printf、print、println、format等)
Java的格式化輸出等同于String.Format,與C有很大的相似,下面這篇文章主要給大家介紹了關(guān)于Java格式化輸出(printf、print、println、format等)的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03maven工程打包引入本地jar包的實(shí)現(xiàn)
我們需要將jar包發(fā)布到一些指定的第三方Maven倉(cāng)庫(kù),本文主要介紹了maven工程打包引入本地jar包的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02java對(duì)象序列化與反序列化的默認(rèn)格式和json格式使用示例
這篇文章主要介紹了java對(duì)象序列化與反序列化的默認(rèn)格式和json格式使用示例,需要的朋友可以參考下2014-02-02