在Vue3中實(shí)現(xiàn)虛擬列表的方法示例
引言
在開(kāi)發(fā)過(guò)程中,我們有時(shí)會(huì)遇到數(shù)據(jù)量較大的情況,這會(huì)導(dǎo)致大量數(shù)據(jù)同時(shí)加載到頁(yè)面,從而生成過(guò)多的 DOM 元素。這種情況不僅會(huì)導(dǎo)致頁(yè)面卡頓,甚至可能導(dǎo)致瀏覽器直接崩潰。給用戶體驗(yàn)帶來(lái)極大的負(fù)面影響。為了解決這一問(wèn)題,我們可以采用虛擬列表技術(shù),通過(guò)只渲染可視區(qū)域內(nèi)的元素,顯著提升頁(yè)面的性能和用戶體驗(yàn)。
現(xiàn)在網(wǎng)上有許多現(xiàn)成的虛擬列表第三方插件庫(kù),我們可以直接使用這些庫(kù)。然而,這邊我打算自己動(dòng)手去實(shí)現(xiàn)虛擬列表功能。在之前的 Vue 2 項(xiàng)目中,我已經(jīng)實(shí)現(xiàn)過(guò)類似的功能,這次我打算利用 Vue 3 來(lái)重新實(shí)現(xiàn),并將其封裝成一個(gè)公共組件。
虛擬列表的基本原理
虛擬列表通過(guò)只渲染當(dāng)前可視區(qū)域內(nèi)的列表項(xiàng),從而提高長(zhǎng)列表加載到頁(yè)面的性能。
- 設(shè)置子數(shù)據(jù)項(xiàng)高度:確定子數(shù)據(jù)項(xiàng)的具體高度。以確定當(dāng)前區(qū)域內(nèi)需要渲染的列表項(xiàng)。
- 計(jì)算可視區(qū)域高度:確定當(dāng)前可視區(qū)域內(nèi)可渲染多少條子數(shù)據(jù)項(xiàng),計(jì)算起始下標(biāo)、結(jié)束下標(biāo)。避免渲染整個(gè)列表。
- 渲染可視區(qū)域:保持渲染的DOM節(jié)點(diǎn)數(shù)量始終在一個(gè)較小的范圍內(nèi),通過(guò)動(dòng)態(tài)調(diào)整渲染內(nèi)容的位置,保持列表高度完整且滾動(dòng)條能正常滾動(dòng)。
- 滾動(dòng)監(jiān)聽(tīng):監(jiān)聽(tīng)容器的滾動(dòng)事件,實(shí)時(shí)獲取滾動(dòng)位置,通過(guò)滾動(dòng)位置實(shí)時(shí)更新可視區(qū)域范圍,動(dòng)態(tài)渲染對(duì)應(yīng)列表項(xiàng)。
- 設(shè)置緩沖列表項(xiàng):在可視區(qū)域的上下各增加一定數(shù)量的緩沖列表項(xiàng),提前加載即將進(jìn)入可視區(qū)域的列表項(xiàng),避免滾動(dòng)時(shí)出現(xiàn)空白以及卡頓的情況。
好的!接下來(lái),我們將通過(guò)代碼一步步實(shí)現(xiàn)上述功能,完整呈現(xiàn)虛擬列表的核心邏輯和效果。
代碼實(shí)現(xiàn)
1、設(shè)置子數(shù)據(jù)項(xiàng)的高度
子數(shù)據(jù)項(xiàng)的高度是固定值,所以這里就定義了個(gè)變量。(注:子數(shù)據(jù)項(xiàng)的高度與css中的高度保持一致)代碼如下:
<script lang="ts" setup> // 子數(shù)據(jù)項(xiàng)高度 const itemHeight = 40 </script>
2、計(jì)算可視區(qū)域高度、起始下標(biāo)、結(jié)束下標(biāo)
因?yàn)橄旅鏁?huì)通過(guò)滾動(dòng)條的高度去計(jì)算詳細(xì)的值。所以這里我們的起始下標(biāo)和結(jié)束下標(biāo)使用計(jì)算屬性去定義。代碼如下:
<script lang="ts" setup>
// 可視區(qū)域的高度
const viewHeight = ref(0)
// ref虛擬列表容器dom
const virtualContainer = ref<HTMLElement | null>(null)
// 在dom加載完成后,通過(guò)ref去獲取可視區(qū)域的高度
onMounted(() => {
nextTick(() => {
viewHeight.value = virtualContainer.value?.clientHeight ?? 0
})
})
// 虛擬列表真實(shí)展示數(shù)據(jù):起始下標(biāo)
const start = computed(() => {
return 0
})
// 虛擬列表真實(shí)展示數(shù)據(jù):結(jié)束下標(biāo)
const end = computed(() => {
return viewHeight.value / itemHeight
})
</script>
3、渲染可視區(qū)域
paddingAttr 的目的是保持列表的高度完整,并確保滾動(dòng)條能夠正常滾動(dòng)。由于實(shí)際渲染的 DOM 元素較少,可能導(dǎo)致滾動(dòng)條位置異常,因此需要通過(guò)設(shè)置 padding 來(lái)?yè)纹鹑萜鞯母叨?。此外,也可以使?transform 和 position 來(lái)實(shí)現(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}` })
}
// 計(jì)算虛擬列表的padding(保持列表高度完整且滾動(dòng)條能正常滾動(dòng))
const paddingAttr = computed(() => {
const paddingTop = start.value * itemHeight
const paddingBottom = (dataList.length - over.value) * itemHeight
return `${paddingTop}px 0 ${paddingBottom}px`
})
// 虛擬列表真實(shí)展示數(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、滾動(dòng)監(jiān)聽(tīng)
上面我們初步的定義了起始下標(biāo)、結(jié)束下標(biāo),但那并不滿足我們的需求,這邊我們通過(guò)監(jiān)聽(tīng)滾動(dòng)事件,獲取到滾動(dòng)條位置,通過(guò)滾動(dòng)條位置去重新計(jì)算起始下標(biāo)、結(jié)束下標(biāo)。代碼如下:
<script lang="ts" setup>
// 滾動(dòng)條距離頂部距離
const scrollTop = ref(0)
// 虛擬列表真實(shí)展示數(shù)據(jù):起始下標(biāo)
const start = computed(() => {
const s = Math.floor(scrollTop.value / itemHeight)
return Math.max(0, s)
})
// 虛擬列表真實(shí)展示數(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)聽(tīng)滾動(dòng)條距離頂部距離,實(shí)時(shí)更新
const onScroll = () => {
scrollTop.value = virtualContainer.value?.scrollTop ?? 0
}
</script>
5、設(shè)置緩沖列表項(xiàng)
這里給起始下標(biāo)和結(jié)束下標(biāo),各自加減一個(gè)固定值,我這邊設(shè)置的值是5,這邊可以設(shè)置成其他值,但不能太大會(huì)影響性能。太小的話滾動(dòng)會(huì)卡頓和出現(xiàn)白屏問(wèn)題。代碼如下:
<script lang="ts" setup>
// 虛擬列表真實(shí)展示數(shù)據(jù):起始下標(biāo)
const start = computed(() => {
const s = Math.floor(scrollTop.value / itemHeight - 5)
return Math.max(0, s)
})
// 虛擬列表真實(shí)展示數(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'
/**
* 虛擬列表的每一項(xiàng)的高度
*/
const itemHeight = 40
const dataList = reactive<any[]>([])
for (let i = 0; i < 100000; i++) {
dataList.push({ id: i, title: `標(biāo)題${i}` })
}
/**
* 滾動(dòng)條距離頂部距離
*/
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í)展示數(shù)據(jù):起始下標(biāo)
*/
const start = computed(() => {
const s = Math.floor(scrollTop.value / itemHeight)
return Math.max(0, s)
})
/**
* 虛擬列表真實(shí)展示數(shù)據(jù):結(jié)束下標(biāo)
*/
const over = computed(() => {
const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight)
return Math.min(dataList.length, o)
})
/**
* 計(jì)算虛擬列表的padding(保持列表高度完整且滾動(dòng)條能正常滾動(dòng))
*/
const paddingAttr = computed(() => {
const paddingTop = start.value * itemHeight
const paddingBottom = (dataList.length - over.value) * itemHeight
return `${paddingTop}px 0 ${paddingBottom}px`
})
/**
* 虛擬列表真實(shí)展示數(shù)據(jù)
*/
const virtualData = computed(() => {
return dataList.slice(start.value, over.value)
})
/**
* 監(jiān)聽(tīng)滾動(dòng)條距離頂部距離,實(shí)時(shí)更新
*/
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>
示例:

組件封裝
上面我們完成了虛擬列表的功能實(shí)現(xiàn),但是呢,在現(xiàn)實(shí)的開(kāi)發(fā)中我們會(huì)遇到不止一個(gè)長(zhǎng)列表的需求,每一個(gè)都這么寫(xiě),會(huì)有很多冗余的代碼,而且很麻煩。所以在這里我們將其封裝成一個(gè)公共的組件。以簡(jiǎn)化我們?nèi)粘i_(kāi)發(fā)的代碼量和時(shí)間成本。
這邊封裝組件的邏輯和上面基本一致,我就不多贅述了,直接上代碼:
<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 每一項(xiàng)的唯一標(biāo)識(shí)key
* @param itemHeight 每一項(xiàng)的高度
* @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%'
})
/**
* 滾動(dòng)條距離頂部距離
*/
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í)展示數(shù)據(jù):起始下標(biāo)
*/
const start = computed(() => {
const s = Math.floor(scrollTop.value / itemHeight - 5)
return Math.max(0, s)
})
/**
* 虛擬列表真實(shí)展示數(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)
})
/**
* 計(jì)算虛擬列表的padding(保持列表高度完整且滾動(dòng)條能正常滾動(dòng))
*/
const paddingAttr = computed(() => {
const paddingTop = start.value * itemHeight
const paddingBottom = (dataList.length - over.value) * itemHeight
return `${paddingTop}px 0 ${paddingBottom}px`
})
/**
* 虛擬列表真實(shí)展示數(shù)據(jù)
*/
const virtualData = computed(() => {
return dataList.slice(start.value, over.value)
})
/**
* 監(jiān)聽(tīng)滾動(dòng)條距離頂部距離,實(shí)時(shí)更新
*/
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>
這邊我們的代碼里面定義了兩個(gè)插槽,default插槽是為了滿足element-ui中的下拉框長(zhǎng)列表問(wèn)題。
代碼如下:
<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中實(shí)現(xiàn)虛擬列表的方法示例的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬列表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue項(xiàng)目中使用vue-i18n報(bào)錯(cuò)的解決方法
這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目中使用vue-i18n報(bào)錯(cuò)的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01
深入解析el-col-group強(qiáng)大且靈活的Element表格列組件
這篇文章主要為大家介紹了el-col-group強(qiáng)大且靈活的Element表格列組件深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
vue第三方庫(kù)中存在擴(kuò)展運(yùn)算符報(bào)錯(cuò)問(wèn)題的解決方案
這篇文章主要介紹了vue第三方庫(kù)中存在擴(kuò)展運(yùn)算符報(bào)錯(cuò)問(wèn)題,本文給大家分享解決方案,通過(guò)結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
Vue監(jiān)聽(tīng)數(shù)組變化源碼解析
這篇文章主要為大家詳細(xì)解析了Vue監(jiān)聽(tīng)數(shù)組變化的源碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
如何使用Vue3實(shí)現(xiàn)文章內(nèi)容中多個(gè)"關(guān)鍵詞"標(biāo)記高亮顯示
高亮顯示是我們?nèi)粘i_(kāi)發(fā)中經(jīng)常會(huì)遇到的需求,下面這篇文章主要給大家介紹了關(guān)于如何使用Vue3實(shí)現(xiàn)文章內(nèi)容中多個(gè)"關(guān)鍵詞"標(biāo)記高亮顯示的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11

