vue虛擬滾動(dòng)性能優(yōu)化方式詳解
引言
一個(gè)簡單的情景模擬(千萬別被帶入):
A: 假設(shè)現(xiàn)在有 10 萬條數(shù)據(jù),你作為前端該怎么優(yōu)化這種大數(shù)據(jù)的列表?
B: 針對大數(shù)據(jù)列表一般不會(huì)依次性加載,會(huì)采用上拉加載、分頁加載等方式實(shí)現(xiàn)優(yōu)化.
A: 那假如加載到最后一條數(shù)據(jù)的時(shí)候,頁面上只是列表部分的數(shù)據(jù)就至少對應(yīng) 10 萬個(gè) dom 節(jié)點(diǎn),你覺得一個(gè)頁面渲染至少 10 萬個(gè) dom 節(jié)點(diǎn)的性能如何?
A: 如果這樣的列表有 n 個(gè)呢?還有沒有別的優(yōu)化方式?
B: 要不我把自己優(yōu)化一下 ......
其實(shí)解決上述問題就可以使用 虛擬滾動(dòng) 來實(shí)現(xiàn)優(yōu)化,相信大家對這個(gè)詞都不陌生,但由于這個(gè)名詞比較短(又是虛擬,又是滾動(dòng))導(dǎo)致很多人覺得這是非常高大上、難以理解的內(nèi)容,但其實(shí)恰恰相反。
本文的目的就是幫助你以 最簡單 的方式 理解虛擬滾動(dòng),甚至實(shí)現(xiàn) 虛擬滾動(dòng).
虛擬滾動(dòng)(Virtual Scrolling)
理解虛擬滾動(dòng)
其實(shí)要理解 虛擬滾動(dòng) 這個(gè)詞很簡單,按照 虛擬 和 滾動(dòng) 兩部分來理解就很簡單了,下面就一一拆解。
虛擬
通常在頁面列表中,要渲染的 列表數(shù)量 和真實(shí)在文檔中存在的 DOM 節(jié)點(diǎn)數(shù) 是 1 : 1 的。
每個(gè)列表項(xiàng)都擁有相同的高度(假設(shè)是 30px),這個(gè)列表容器中需要完全渲染的列表數(shù)(假設(shè)是 100 條)和在頁面中的高度是一致的,即此時(shí)的高度就為 100 * 30 = 300 px,對應(yīng)列表的 DOM 數(shù)量為 100,如:
對于 虛擬滾動(dòng) 來講,虛擬 的意思是指實(shí)際要渲染完整列表對應(yīng)的高度是通過 虛擬計(jì)算 的,并不是指文檔中存在對應(yīng)的 DOM 節(jié)點(diǎn)數(shù)。
上面的栗子對應(yīng)到虛擬滾動(dòng)來講,就意味著實(shí)際渲染完整列表對應(yīng)的高度就仍為 100 * 30 = 300px,但實(shí)際渲染數(shù)就變?yōu)?10 條,關(guān)系圖大致如下:
滾動(dòng)
所謂 滾動(dòng) 就很好理解了,因?yàn)榱斜砜梢晠^(qū)通常會(huì)限制一定的高度,即 列表可視區(qū)高度,那么此時(shí)只要 虛擬列表高度 值大于 列表可視區(qū)高度 時(shí),就會(huì)產(chǎn)生滾動(dòng)條即可發(fā)生滾動(dòng)操作。
值得注意的是,在發(fā)生滾動(dòng)時(shí)需要對 實(shí)際渲染的列表 進(jìn)行一些處理,否則會(huì)出現(xiàn) 實(shí)際渲染的列表 和 虛擬列表區(qū) 脫離的情況,比如:
關(guān)鍵點(diǎn)就是實(shí)現(xiàn)在發(fā)生 滾動(dòng) 操作時(shí),保證 實(shí)際渲染的列表 一直存在 列表可視區(qū) 中,并且動(dòng)態(tài)切換需要渲染的列表數(shù)據(jù)。
實(shí)現(xiàn)虛擬滾動(dòng)
核心步驟
- 設(shè)置列表可視區(qū)的高度 containerHeight
- 設(shè)置單個(gè)列表項(xiàng)的高度 listItemHeight
- 計(jì)算渲染完整列表需要的高度 virtualHeight,即 virtualHeight = listItemHeight * data.length
- 設(shè)置真實(shí)渲染數(shù)據(jù)的起始索引 startIndex、endIndex,用于從列表數(shù)據(jù) data 中獲取對應(yīng)的數(shù)據(jù)內(nèi)容
- 注冊/監(jiān)聽滾動(dòng)事件 onScroll
- 獲取當(dāng)前實(shí)際滾動(dòng)距離 eleScrollTop
- 將 eleScrollTop 作為 translateY 的值,即 實(shí)際渲染列表元素 平移的數(shù)值,保證 實(shí)際渲染列表元素 一直存在可視區(qū)中
- 根據(jù)實(shí)際的滾動(dòng)距離 eleScrollTop,動(dòng)態(tài)計(jì)算列表新的起始索引 startIndex、endIndex
效果預(yù)覽
具體實(shí)現(xiàn)細(xì)節(jié)都在如下的代碼中,可結(jié)合其中的注釋閱讀:
// 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> <!-- 虛擬滾動(dòng)內(nèi)容 --> <div class="virtual-scroller" @scroll="onScroll" :style="{ height: containerHeight + 'px' }" > <!-- 實(shí)際渲染的列表內(nèi)容 --> <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' }"> ~ 數(shù)據(jù)加載完畢 ~ </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); // 結(jié)束索引 // 實(shí)際渲染的數(shù)據(jù) const visibleList = computed(() => { return data.slice(startIndex.value, endIndex.value); }); // 虛擬滾動(dòng)的高度 const virtualHeight = computed(() => { return (data.length - visibleList.value.length) * listItemHeight + listItemHeight; }); // 滾動(dòng)事件 const onScroll = (e) => { const eleScrollTop = e.target.scrollTop; // 保證實(shí)際渲染列表一直停留在可視區(qū) tranlateY.value = eleScrollTop; // 根據(jù)實(shí)際的滾動(dòng)距離,動(dòng)態(tài)計(jì)算列表開始索引 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>
最后
其實(shí) 虛擬滾動(dòng) 并不難理解,就像 CSS 中的 BFC、JavaScript 中的閉包 等概念一樣,最初了解時(shí)你很難給它一個(gè)定義,但是實(shí)際上下功夫去了解它,其實(shí)也就那么一回事。
以上的實(shí)現(xiàn)方式是極簡的方式,沒有做任何的優(yōu)化、沒有考慮額外的場景,因?yàn)楸疚牡哪康倪€是想通過最簡單的實(shí)現(xiàn)去解釋虛擬滾動(dòng)到底是怎么一回事,因此不必過于糾結(jié),當(dāng)然在 vue 中早已有了相關(guān)庫的實(shí)現(xiàn) vue-virtual-scroller 可自行了解,更多關(guān)于vue虛擬滾動(dòng)性能優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
不依任何賴第三方,單純用vue實(shí)現(xiàn)Tree 樹形控件的案例
這篇文章主要介紹了不依任何賴第三方,單純用vue實(shí)現(xiàn)Tree 樹形控件的案例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09vue項(xiàng)目打包后請求地址錯(cuò)誤/打包后跨域操作
這篇文章主要介紹了vue項(xiàng)目打包后請求地址錯(cuò)誤/打包后跨域操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11vue2封裝webSocket的實(shí)現(xiàn)(開箱即用)
在Vue2中,可以使用WebSocket實(shí)時(shí)通信,本文主要介紹了vue2封裝webSocket的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08解決vue2中使用elementUi打包報(bào)錯(cuò)的問題
這篇文章主要介紹了解決vue2中使用elementUi打包報(bào)錯(cuò)的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09詳解vue-cli 構(gòu)建項(xiàng)目 vue-cli請求后臺(tái)接口 vue-cli使用axios、sass、swiper
本文通過實(shí)例代碼給大家詳細(xì)介紹了vue-cli 構(gòu)建項(xiàng)目 vue-cli請求后臺(tái)接口 vue-cli使用axios、sass、swiper的相關(guān)知識(shí),需要的朋友可以參考下2018-05-05Vue2?Observer實(shí)例dep和閉包中dep區(qū)別詳解
這篇文章主要為大家介紹了Vue2?Observer實(shí)例dep和閉包中dep區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10