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

文章小尾巴
以上就是在Vue3中實現(xiàn)虛擬列表的方法示例的詳細內(nèi)容,更多關(guān)于Vue3虛擬列表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入解析el-col-group強大且靈活的Element表格列組件
這篇文章主要為大家介紹了el-col-group強大且靈活的Element表格列組件深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04
如何使用Vue3實現(xiàn)文章內(nèi)容中多個"關(guān)鍵詞"標記高亮顯示
高亮顯示是我們?nèi)粘i_發(fā)中經(jīng)常會遇到的需求,下面這篇文章主要給大家介紹了關(guān)于如何使用Vue3實現(xiàn)文章內(nèi)容中多個"關(guān)鍵詞"標記高亮顯示的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-11-11

