在Vue3中實(shí)現(xiàn)虛擬列表的方法示例
引言
在開發(fā)過程中,我們有時(shí)會(huì)遇到數(shù)據(jù)量較大的情況,這會(huì)導(dǎo)致大量數(shù)據(jù)同時(shí)加載到頁(yè)面,從而生成過多的 DOM 元素。這種情況不僅會(huì)導(dǎo)致頁(yè)面卡頓,甚至可能導(dǎo)致瀏覽器直接崩潰。給用戶體驗(yàn)帶來極大的負(fù)面影響。為了解決這一問題,我們可以采用虛擬列表技術(shù),通過只渲染可視區(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)過類似的功能,這次我打算利用 Vue 3 來重新實(shí)現(xiàn),并將其封裝成一個(gè)公共組件。
虛擬列表的基本原理
虛擬列表通過只渲染當(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),通過動(dòng)態(tài)調(diào)整渲染內(nèi)容的位置,保持列表高度完整且滾動(dòng)條能正常滾動(dòng)。
- 滾動(dòng)監(jiān)聽:監(jiān)聽容器的滾動(dòng)事件,實(shí)時(shí)獲取滾動(dòng)位置,通過滾動(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)空白以及卡頓的情況。
好的!接下來,我們將通過代碼一步步實(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ì)通過滾動(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加載完成后,通過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)條位置異常,因此需要通過設(shè)置 padding
來?yè)纹鹑萜鞯母叨?。此外,也可以使?transform
和 position
來實(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)聽
上面我們初步的定義了起始下標(biāo)、結(jié)束下標(biāo),但那并不滿足我們的需求,這邊我們通過監(jiān)聽滾動(dòng)事件,獲取到滾動(dòng)條位置,通過滾動(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)聽滾動(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)白屏問題。代碼如下:
<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)聽滾動(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í)的開發(fā)中我們會(huì)遇到不止一個(gè)長(zhǎng)列表的需求,每一個(gè)都這么寫,會(huì)有很多冗余的代碼,而且很麻煩。所以在這里我們將其封裝成一個(gè)公共的組件。以簡(jiǎn)化我們?nèi)粘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)聽滾動(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)列表問題。
代碼如下:
<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ò)的解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01深入解析el-col-group強(qiáng)大且靈活的Element表格列組件
這篇文章主要為大家介紹了el-col-group強(qiáng)大且靈活的Element表格列組件深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04vue第三方庫(kù)中存在擴(kuò)展運(yùn)算符報(bào)錯(cuò)問題的解決方案
這篇文章主要介紹了vue第三方庫(kù)中存在擴(kuò)展運(yùn)算符報(bào)錯(cuò)問題,本文給大家分享解決方案,通過結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07如何使用Vue3實(shí)現(xiàn)文章內(nèi)容中多個(gè)"關(guān)鍵詞"標(biāo)記高亮顯示
高亮顯示是我們?nèi)粘i_發(fā)中經(jīng)常會(huì)遇到的需求,下面這篇文章主要給大家介紹了關(guān)于如何使用Vue3實(shí)現(xiàn)文章內(nèi)容中多個(gè)"關(guān)鍵詞"標(biāo)記高亮顯示的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11