在Vue3中實現(xiàn)虛擬列表的方法示例
引言
在開發(fā)過程中,我們有時會遇到數(shù)據(jù)量較大的情況,這會導(dǎo)致大量數(shù)據(jù)同時加載到頁面,從而生成過多的 DOM 元素。這種情況不僅會導(dǎo)致頁面卡頓,甚至可能導(dǎo)致瀏覽器直接崩潰。給用戶體驗帶來極大的負(fù)面影響。為了解決這一問題,我們可以采用虛擬列表技術(shù),通過只渲染可視區(qū)域內(nèi)的元素,顯著提升頁面的性能和用戶體驗。
現(xiàn)在網(wǎng)上有許多現(xiàn)成的虛擬列表第三方插件庫,我們可以直接使用這些庫。然而,這邊我打算自己動手去實現(xiàn)虛擬列表功能。在之前的 Vue 2 項目中,我已經(jīng)實現(xiàn)過類似的功能,這次我打算利用 Vue 3 來重新實現(xiàn),并將其封裝成一個公共組件。
虛擬列表的基本原理
虛擬列表通過只渲染當(dāng)前可視區(qū)域內(nèi)的列表項,從而提高長列表加載到頁面的性能。
- 設(shè)置子數(shù)據(jù)項高度:確定子數(shù)據(jù)項的具體高度。以確定當(dāng)前區(qū)域內(nèi)需要渲染的列表項。
- 計算可視區(qū)域高度:確定當(dāng)前可視區(qū)域內(nèi)可渲染多少條子數(shù)據(jù)項,計算起始下標(biāo)、結(jié)束下標(biāo)。避免渲染整個列表。
- 渲染可視區(qū)域:保持渲染的DOM節(jié)點數(shù)量始終在一個較小的范圍內(nèi),通過動態(tài)調(diào)整渲染內(nèi)容的位置,保持列表高度完整且滾動條能正常滾動。
- 滾動監(jiān)聽:監(jiān)聽容器的滾動事件,實時獲取滾動位置,通過滾動位置實時更新可視區(qū)域范圍,動態(tài)渲染對應(yīng)列表項。
- 設(shè)置緩沖列表項:在可視區(qū)域的上下各增加一定數(shù)量的緩沖列表項,提前加載即將進(jìn)入可視區(qū)域的列表項,避免滾動時出現(xiàn)空白以及卡頓的情況。
好的!接下來,我們將通過代碼一步步實現(xiàn)上述功能,完整呈現(xiàn)虛擬列表的核心邏輯和效果。
代碼實現(xiàn)
1、設(shè)置子數(shù)據(jù)項的高度
子數(shù)據(jù)項的高度是固定值,所以這里就定義了個變量。(注:子數(shù)據(jù)項的高度與css中的高度保持一致)代碼如下:
<script lang="ts" setup> // 子數(shù)據(jù)項高度 const itemHeight = 40 </script>
2、計算可視區(qū)域高度、起始下標(biāo)、結(jié)束下標(biāo)
因為下面會通過滾動條的高度去計算詳細(xì)的值。所以這里我們的起始下標(biāo)和結(jié)束下標(biāo)使用計算屬性去定義。代碼如下:
<script lang="ts" setup> // 可視區(qū)域的高度 const viewHeight = ref(0) // ref虛擬列表容器dom const virtualContainer = ref<HTMLElement | null>(null) // 在dom加載完成后,通過ref去獲取可視區(qū)域的高度 onMounted(() => { nextTick(() => { viewHeight.value = virtualContainer.value?.clientHeight ?? 0 }) }) // 虛擬列表真實展示數(shù)據(jù):起始下標(biāo) const start = computed(() => { return 0 }) // 虛擬列表真實展示數(shù)據(jù):結(jié)束下標(biāo) const end = computed(() => { return viewHeight.value / itemHeight }) </script>
3、渲染可視區(qū)域
paddingAttr
的目的是保持列表的高度完整,并確保滾動條能夠正常滾動。由于實際渲染的 DOM 元素較少,可能導(dǎo)致滾動條位置異常,因此需要通過設(shè)置 padding
來撐起容器的高度。此外,也可以使用 transform
和 position
來實現(xiàn)這一效果。代碼如下:
<div ref="virtualContainer" @scroll="onScroll" class="virtual-container"> <div class="virtual-list"> <div class="virtual-item" v-for="item in virtualData" :key="item.id"> <div class="item">{{ item.title }}</div> </div> </div> </div> <script lang="ts" setup> // 大數(shù)據(jù)數(shù)組 const dataList = reactive<any[]>([]) for (let i = 0; i < 100000; i++) { dataList.push({ id: i, title: `標(biāo)題${i}` }) } // 計算虛擬列表的padding(保持列表高度完整且滾動條能正常滾動) const paddingAttr = computed(() => { const paddingTop = start.value * itemHeight const paddingBottom = (dataList.length - over.value) * itemHeight return `${paddingTop}px 0 ${paddingBottom}px` }) // 虛擬列表真實展示數(shù)據(jù) const virtualData = computed(() => { return dataList.slice(start.value, over.value) }) </script> <style lang="scss" scoped> .virtual-container { overflow-y: auto; height: 100%; .virtual-list { padding: v-bind(paddingAttr); .virtual-item { text-align: center; height: 30px; line-height: 30px; background: #84bbfc; margin-bottom: 10px; } } } </style>
4、滾動監(jiān)聽
上面我們初步的定義了起始下標(biāo)、結(jié)束下標(biāo),但那并不滿足我們的需求,這邊我們通過監(jiān)聽滾動事件,獲取到滾動條位置,通過滾動條位置去重新計算起始下標(biāo)、結(jié)束下標(biāo)。代碼如下:
<script lang="ts" setup> // 滾動條距離頂部距離 const scrollTop = ref(0) // 虛擬列表真實展示數(shù)據(jù):起始下標(biāo) const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight) return Math.max(0, s) }) // 虛擬列表真實展示數(shù)據(jù):結(jié)束下標(biāo) const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight) return Math.min(dataList.length, o) }) // 監(jiān)聽滾動條距離頂部距離,實時更新 const onScroll = () => { scrollTop.value = virtualContainer.value?.scrollTop ?? 0 } </script>
5、設(shè)置緩沖列表項
這里給起始下標(biāo)和結(jié)束下標(biāo),各自加減一個固定值,我這邊設(shè)置的值是5,這邊可以設(shè)置成其他值,但不能太大會影響性能。太小的話滾動會卡頓和出現(xiàn)白屏問題。代碼如下:
<script lang="ts" setup> // 虛擬列表真實展示數(shù)據(jù):起始下標(biāo) const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight - 5) return Math.max(0, s) }) // 虛擬列表真實展示數(shù)據(jù):結(jié)束下標(biāo) const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight + 5) return Math.min(dataList.length, o) }) </script>
好了,下面是虛擬列表的完整的代碼:
<template> <div ref="virtualContainer" @scroll="onScroll" class="virtual-container"> <div class="virtual-list"> <div class="virtual-item" v-for="item in virtualData" :key="item.id"> <div class="item">{{ item.title }}</div> </div> </div> </div> </template> <script lang="ts" setup> import { computed, nextTick, onMounted, ref, reactive } from 'vue' /** * 虛擬列表的每一項的高度 */ const itemHeight = 40 const dataList = reactive<any[]>([]) for (let i = 0; i < 100000; i++) { dataList.push({ id: i, title: `標(biāo)題${i}` }) } /** * 滾動條距離頂部距離 */ const scrollTop = ref(0) /** * ref虛擬列表容器dom */ const virtualContainer = ref<HTMLElement | null>(null) /** * 可視區(qū)域的高度 */ const viewHeight = ref(0) // 在dom加載完成后,獲取可視區(qū)域的高度 onMounted(() => { nextTick(() => { viewHeight.value = virtualContainer.value?.clientHeight ?? 0 }) }) /** * 虛擬列表真實展示數(shù)據(jù):起始下標(biāo) */ const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight) return Math.max(0, s) }) /** * 虛擬列表真實展示數(shù)據(jù):結(jié)束下標(biāo) */ const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight) return Math.min(dataList.length, o) }) /** * 計算虛擬列表的padding(保持列表高度完整且滾動條能正常滾動) */ const paddingAttr = computed(() => { const paddingTop = start.value * itemHeight const paddingBottom = (dataList.length - over.value) * itemHeight return `${paddingTop}px 0 ${paddingBottom}px` }) /** * 虛擬列表真實展示數(shù)據(jù) */ const virtualData = computed(() => { return dataList.slice(start.value, over.value) }) /** * 監(jiān)聽滾動條距離頂部距離,實時更新 */ const onScroll = () => { scrollTop.value = virtualContainer.value?.scrollTop ?? 0 } </script> <style lang="scss" scoped> .virtual-container { overflow-y: auto; height: 100%; .virtual-list { padding: v-bind(paddingAttr); .virtual-item { text-align: center; height: 30px; line-height: 30px; background: #84bbfc; margin-bottom: 10px; } } } ::-webkit-scrollbar { width: 12px; height: 12px; background: #ffffff; border-radius: 6px; } ::-webkit-scrollbar-thumb { background: #00a6ff; border-radius: 6px; } </style>
示例:
組件封裝
上面我們完成了虛擬列表的功能實現(xiàn),但是呢,在現(xiàn)實的開發(fā)中我們會遇到不止一個長列表的需求,每一個都這么寫,會有很多冗余的代碼,而且很麻煩。所以在這里我們將其封裝成一個公共的組件。以簡化我們?nèi)粘i_發(fā)的代碼量和時間成本。
這邊封裝組件的邏輯和上面基本一致,我就不多贅述了,直接上代碼:
<template> <div ref="virtualContainer" @scroll="onScroll" class="virtual-container"> <div class="virtual-list"> <slot v-if="slotDefault" name="default" :dataList="virtualData"></slot> <template v-else> <div class="virtual-item" v-for="item in virtualData" :key="item[keyField]" :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }" > <slot name="item" :item="item"></slot> </div> </template> </div> </div> </template> <script lang="ts" setup name="VirtualList"> import { withDefaults, defineProps, computed, nextTick, onMounted, ref, useSlots } from 'vue' /** * 虛擬列表defineProps接口(類型約束) * @param dataList 數(shù)據(jù)列表 * @param keyField 每一項的唯一標(biāo)識key * @param itemHeight 每一項的高度 * @param containerHeight 容器高度 */ interface virtualProps { dataList: any[] keyField?: string itemHeight?: number containerHeight?: string } /** * 父組件傳入的值 * withDefaults 為props設(shè)置默認(rèn)值 */ const { dataList, keyField, itemHeight, containerHeight } = withDefaults(defineProps<virtualProps>(), { keyField: 'id', itemHeight: 40, containerHeight: '100%' }) /** * 滾動條距離頂部距離 */ const scrollTop = ref(0) /** * ref虛擬列表容器dom */ const virtualContainer = ref<HTMLElement | null>(null) /** * 可視區(qū)域的高度 */ const viewHeight = ref(0) onMounted(() => { nextTick(() => { viewHeight.value = virtualContainer.value?.clientHeight ?? 0 }) }) /** * 虛擬列表真實展示數(shù)據(jù):起始下標(biāo) */ const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight - 5) return Math.max(0, s) }) /** * 虛擬列表真實展示數(shù)據(jù):結(jié)束下標(biāo) */ const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight + 5) return Math.min(dataList.length, o) }) /** * 計算虛擬列表的padding(保持列表高度完整且滾動條能正常滾動) */ const paddingAttr = computed(() => { const paddingTop = start.value * itemHeight const paddingBottom = (dataList.length - over.value) * itemHeight return `${paddingTop}px 0 ${paddingBottom}px` }) /** * 虛擬列表真實展示數(shù)據(jù) */ const virtualData = computed(() => { return dataList.slice(start.value, over.value) }) /** * 監(jiān)聽滾動條距離頂部距離,實時更新 */ const onScroll = () => { scrollTop.value = virtualContainer.value?.scrollTop ?? 0 } /** * 獲取默認(rèn)插槽 */ const slotDefault = useSlots().default </script> <style lang="scss" scoped> .virtual-container { overflow-y: auto; height: v-bind(containerHeight); .virtual-list { padding: v-bind(paddingAttr); .virtual-item { text-align: center; border: 1px solid orangered; } } } ::-webkit-scrollbar { width: 12px; height: 12px; background: #ffffff; border-radius: 6px; } ::-webkit-scrollbar-thumb { background: #00a6ff; border-radius: 6px; } </style>
這邊我們的代碼里面定義了兩個插槽,default
插槽是為了滿足element-ui
中的下拉框長列表問題。
代碼如下:
<template> <div style="height: 100%"> <div style="width: 240px; height: 100%"> <el-select multiple v-model="activeName" @visible-change="visibleChange"> <VirtualList v-if="visibleState" :data-list="data" :item-height="34" container-height="194px"> <template #default="{ dataList }"> <el-option v-for="i in dataList" :label="i.title" :value="i.id" :key="i.id" /> </template> </VirtualList> </el-select> </div> </div> </template> <script lang="ts" setup> import VirtualList from '@/components/VirtualList/index.vue' import { reactive, ref } from 'vue' const data = reactive<any[]>([]) for (let i = 0; i < 100000; i++) { data.push({ id: i, title: `標(biāo)題${i}` }) } const activeName = ref('') const visibleState = ref(false) const visibleChange = (val: boolean) => { visibleState.value = val } </script>
文章小尾巴
以上就是在Vue3中實現(xiàn)虛擬列表的方法示例的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬列表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入解析el-col-group強大且靈活的Element表格列組件
這篇文章主要為大家介紹了el-col-group強大且靈活的Element表格列組件深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04如何使用Vue3實現(xiàn)文章內(nèi)容中多個"關(guān)鍵詞"標(biāo)記高亮顯示
高亮顯示是我們?nèi)粘i_發(fā)中經(jīng)常會遇到的需求,下面這篇文章主要給大家介紹了關(guān)于如何使用Vue3實現(xiàn)文章內(nèi)容中多個"關(guān)鍵詞"標(biāo)記高亮顯示的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11