vue虛擬滾動性能優化方式詳解
引言
一個簡單的情景模擬(千萬別被帶入):
A: 假設現在有 10 萬條數據,你作為前端該怎麼優化這種大數據的列表?
B: 針對大數據列表一般不會依次性加載,會采用上拉加載、分頁加載等方式實現優化.
A: 那假如加載到最後一條數據的時候,頁面上隻是列表部分的數據就至少對應 10 萬個 dom 節點,你覺得一個頁面渲染至少 10 萬個 dom 節點的性能如何?
A: 如果這樣的列表有 n 個呢?還有沒有別的優化方式?
B: 要不我把自己優化一下 ……
其實解決上述問題就可以使用 虛擬滾動 來實現優化,相信大傢對這個詞都不陌生,但由於這個名詞比較短(又是虛擬,又是滾動)導致很多人覺得這是非常高大上、難以理解的內容,但其實恰恰相反。
本文的目的就是幫助你以 最簡單 的方式 理解虛擬滾動,甚至實現 虛擬滾動.
虛擬滾動(Virtual Scrolling)
理解虛擬滾動
其實要理解 虛擬滾動 這個詞很簡單,按照 虛擬 和 滾動 兩部分來理解就很簡單瞭,下面就一一拆解。
虛擬
通常在頁面列表中,要渲染的 列表數量 和真實在文檔中存在的 DOM 節點數 是 1 : 1 的。
每個列表項都擁有相同的高度(假設是 30px),這個列表容器中需要完全渲染的列表數(假設是 100 條)和在頁面中的高度是一致的,即此時的高度就為 100 * 30 = 300 px,對應列表的 DOM 數量為 100,如:
對於 虛擬滾動 來講,虛擬 的意思是指實際要渲染完整列表對應的高度是通過 虛擬計算 的,並不是指文檔中存在對應的 DOM 節點數。
上面的栗子對應到虛擬滾動來講,就意味著實際渲染完整列表對應的高度就仍為 100 * 30 = 300px,但實際渲染數就變為 10 條,關系圖大致如下:
滾動
所謂 滾動 就很好理解瞭,因為列表可視區通常會限制一定的高度,即 列表可視區高度,那麼此時隻要 虛擬列表高度 值大於 列表可視區高度 時,就會產生滾動條即可發生滾動操作。
值得註意的是,在發生滾動時需要對 實際渲染的列表 進行一些處理,否則會出現 實際渲染的列表 和 虛擬列表區 脫離的情況,比如:
關鍵點就是實現在發生 滾動 操作時,保證 實際渲染的列表 一直存在 列表可視區 中,並且動態切換需要渲染的列表數據。
實現虛擬滾動
核心步驟
- 設置列表可視區的高度 containerHeight
- 設置單個列表項的高度 listItemHeight
- 計算渲染完整列表需要的高度 virtualHeight,即 virtualHeight = listItemHeight * data.length
- 設置真實渲染數據的起始索引 startIndex、endIndex,用於從列表數據 data 中獲取對應的數據內容
- 註冊/監聽滾動事件 onScroll
- 獲取當前實際滾動距離 eleScrollTop
- 將 eleScrollTop 作為 translateY 的值,即 實際渲染列表元素 平移的數值,保證 實際渲染列表元素 一直存在可視區中
- 根據實際的滾動距離 eleScrollTop,動態計算列表新的起始索引 startIndex、endIndex
效果預覽
具體實現細節都在如下的代碼中,可結合其中的註釋閱讀:
// App.vue <script> const list = (num = 10)=> { const data = []; for (let i = 0; i < num; i++) { data.push({ id: i+1, name: `第 ${i+1} 條列表` }); } return data; } </script> <template> <VirtualScroll :data="list(100)" /> </template> // VirtualScroll.vue <template> <!-- 虛擬滾動內容 --> <div class="virtual-scroller" @scroll="onScroll" :style="{ height: containerHeight + 'px' }" > <!-- 實際渲染的列表內容 --> <ul class="real-list-content" :style="{ transform: `translateY(${tranlateY}px)` }" > <li v-for="item in visibleList" :key="item.id" :style="{ height: `${listItemHeight}px`, 'line-height': `${listItemHeight}px`, }" > <div>{{ item.name }}</div> </li> </ul> <!-- 虛擬列表元素 --> <div class="virtual-height" :style="{ height: virtualHeight + 'px' }"> ~ 數據加載完畢 ~ </div> </div> </template> <script> export default { name: "vue-virtual-scroll", }; </script> <script setup language="ts"> import { computed, ref } from "vue"; const props = defineProps({ data: { type: Array, default: [], }, startIndex: { type: Number, default: 0, }, endIndex: { type: Number, default: 10, }, listItemHeight: { type: Number, default: 60, }, containerHeight: { type: Number, default: 500, }, }); let { data, listItemHeight } = props; const tranlateY = ref(0); // 平移距離 const startIndex = ref(props.startIndex); // 開始索引 const endIndex = ref(props.endIndex); // 結束索引 // 實際渲染的數據 const visibleList = computed(() => { return data.slice(startIndex.value, endIndex.value); }); // 虛擬滾動的高度 const virtualHeight = computed(() => { return (data.length - visibleList.value.length) * listItemHeight + listItemHeight; }); // 滾動事件 const onScroll = (e) => { const eleScrollTop = e.target.scrollTop; // 保證實際渲染列表一直停留在可視區 tranlateY.value = eleScrollTop; // 根據實際的滾動距離,動態計算列表開始索引 startIndex.value = Math.floor(eleScrollTop / listItemHeight); // 基於開始索引 endIndex.value = startIndex.value + 10; }; </script> <style scoped> .virtual-scroller { border: solid 1px #eee; margin-top: 10px; height: 600px; overflow: auto; } .virtual-height { background: red; display: flex; align-items: end; justify-content: center; color: #fff; } ul { list-style: none; padding: 0; margin: 0; } li { outline: solid 1px #fff; background-color: #000; color: #fff; } </style>
最後
其實 虛擬滾動 並不難理解,就像 CSS 中的 BFC、JavaScript 中的閉包 等概念一樣,最初瞭解時你很難給它一個定義,但是實際上下功夫去瞭解它,其實也就那麼一回事。
以上的實現方式是極簡的方式,沒有做任何的優化、沒有考慮額外的場景,因為本文的目的還是想通過最簡單的實現去解釋虛擬滾動到底是怎麼一回事,因此不必過於糾結,當然在 vue 中早已有瞭相關庫的實現 vue-virtual-scroller 可自行瞭解,更多關於vue虛擬滾動性能優化的資料請關註WalkonNet其它相關文章!