Vue3實(shí)現(xiàn)動(dòng)態(tài)高度的虛擬滾動(dòng)列表的示例代碼
什么是虛擬滾動(dòng)列表
虛擬滾動(dòng)列表是一種優(yōu)化長(zhǎng)列表渲染性能的技術(shù),通過只渲染可視區(qū)域內(nèi)的列表項(xiàng),減少DOM的渲染數(shù)量,從而提高頁(yè)面滾動(dòng)的流暢性。
核心原理
在滾動(dòng)時(shí),只渲染可視區(qū)域內(nèi)的列表項(xiàng),而不是一次性渲染所有列表項(xiàng)。通過計(jì)算可視區(qū)域的起始索引和結(jié)束索引,動(dòng)態(tài)渲染該范圍內(nèi)的列表項(xiàng)。當(dāng)用戶滾動(dòng)時(shí),根據(jù)滾動(dòng)位置動(dòng)態(tài)更新可視區(qū)域內(nèi)的列表項(xiàng),從而實(shí)現(xiàn)虛擬滾動(dòng)的效果。
圖例:
實(shí)現(xiàn)思路
為了實(shí)現(xiàn)虛擬滾動(dòng)列表,需要設(shè)計(jì)三個(gè)盒子(可視區(qū)盒子、列表容器、列表項(xiàng)容器)
- 可視區(qū)盒子,控制列表展示的區(qū)域,超出滾動(dòng);
- 列表容器,包裹所有列表項(xiàng)的盒子,高度為真實(shí)列表的高度;
- 列表項(xiàng)容器,每個(gè)列表項(xiàng)外的盒子,用于動(dòng)態(tài)定位展示列表項(xiàng);
解釋:監(jiān)聽1中盒子的滾動(dòng)事件,在滾動(dòng)的時(shí)候通過事件對(duì)象中的scrollTop動(dòng)態(tài)計(jì)算對(duì)應(yīng)展示區(qū)列表項(xiàng)的開始索引和結(jié)束索引,通過列表項(xiàng)的高度計(jì)算出對(duì)應(yīng)列表項(xiàng)的偏移量,更新展示區(qū)域的列表項(xiàng);
代碼實(shí)現(xiàn)
這里需要計(jì)算所有列表項(xiàng)外包裹的盒子的高度以及每一項(xiàng)列表偏移量,但是由于我們要渲染的列表項(xiàng)每一項(xiàng)的高度都是不等長(zhǎng)的,所以我們只有在對(duì)應(yīng)列表項(xiàng)DOM渲染完成之后才知道每一項(xiàng)的真實(shí)高度,然后我們需要維護(hù)一個(gè)所有列表項(xiàng)對(duì)應(yīng)的高度和偏移量的map數(shù)據(jù),用于更新列表,每次在真實(shí)的列表項(xiàng)掛在之后動(dòng)態(tài)去更新這個(gè)map數(shù)據(jù),再以此為模板動(dòng)態(tài)更新包裹的盒子的高度;
list-item容器代碼
<!-- list-item.vue --> <template> <div :style="style" ref="domRef"> <slot name="slot-scope" :data="data"></slot> </div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import ResizeObserver from 'resize-observer-polyfill'; // ResizeObserver兼容低版本瀏覽器 if (window.ResizeObserver === undefined) { window.ResizeObserver = ResizeObserver; } const emit = defineEmits(['onSizeChange']); const props = defineProps({ style: { type: Object, default: () => { } }, data: { type: Object, default: () => { } }, index: { type: Number, default: 0 } }) const domRef = ref(null); const resizeObserver = null; onMounted(() => { const domNode = domRef.value.children[0]; emit("onSizeChange", props.index, domNode); const resizeObserver = new ResizeObserver(() => { emit("onSizeChange", props.index, domNode); }); resizeObserver.observe(domNode); }) onUnmounted(() => { if (resizeObserver) { resizeObserver?.unobserve(domRef.value.children[0]); } }) </script>
此處使用ResizeObserver
這個(gè)api來監(jiān)聽列表項(xiàng)dom尺寸的改變,用于更新我們維護(hù)的列表項(xiàng)map,但是這個(gè)api在低版本瀏覽器會(huì)有兼容性問題,導(dǎo)致白屏報(bào)錯(cuò),可安裝resize-observer-polyfill兼容低版本瀏覽器;
list容器代碼
<template> <div class="virtual-wrap" :class="{ hideScrollBar: isHideScrollBar }" ref="virtualWrap" :style="{ width: width, height: height, }" @scroll="scrollHandle" > <div class="virtual-content" :style="{height: totalEstimatedHeight +'px'}"> <list-item v-for="(item,index) in showItemList" :key="item.dataIndex+index" :index="item.dataIndex" :data="item.data" :style="item.style" @onSizeChange="sizeChangeHandle"> <template #slot-scope="slotProps"> <slot name="slot-scope" :slotProps="slotProps"></slot> </template> </list-item> </div> </div> </template> <script setup> import ListItem from './list-item.vue'; import { ref, onMounted,watch, nextTick } from 'vue' const props = defineProps({ isHideScrollBar: { type: Boolean, default: false }, height: { default: 100, type: Number }, width: { default: 100, type: Number }, itemEstimatedSize: { default: 50, type: Number }, itemCount: { default: 0, type: Number }, data: { default: ()=>[], type: Array }, buffCount:{ default: 4, type: Number } }) const virtualWrap = ref(null); const showItemList = ref([]); const totalEstimatedHeight=ref(0) const scrollOffset = ref(0) watch(props.data,()=>{ getCurrentChildren() }) const sizeChangeHandle = (index, domNode) => { const height = domNode.offsetHeight; const { measuredDataMap, lastMeasuredItemIndex } = measuredData; const itemMetaData = measuredDataMap[index]; itemMetaData.size = height; let offset = 0; for (let i = 0; i <= lastMeasuredItemIndex; i++) { const itemData = measuredDataMap[i]; itemData.offset = offset; offset += itemData.size; } } // 元數(shù)據(jù) const measuredData = { measuredDataMap: {}, lastMeasuredItemIndex: -1, }; const getCurrentChildren = () => { //重新計(jì)算高度 estimatedHeight(props.itemEstimatedSize,props.itemCount) const [startIndex, endIndex] = getRangeToRender(props, scrollOffset.value) const items = []; for (let i = startIndex; i <= endIndex; i++) { const item = getItemMetaData(i); const itemStyle = { position: 'absolute', height: item.size + 'px', width: '100%', top: item.offset + 'px', }; items.push({ style: itemStyle, data: props.data[i], dataIndex:i }); } showItemList.value = items; } const getRangeToRender = (props, scrollOffset) => { const { itemCount } = props; const startIndex = getStartIndex(props, scrollOffset); const endIndex = getEndIndex(props, startIndex + props.buffCount); return [ Math.max(0, startIndex -1 - props.buffCount), Math.min(itemCount - 1, endIndex ), ]; }; const getStartIndex = (props, scrollOffset) => { const { itemCount } = props; let index = 0; while (true) { const currentOffset = getItemMetaData(index).offset; if (currentOffset >= scrollOffset) return index; if (index >= itemCount) return itemCount; index++ } } const getItemMetaData = (index) => { const { itemEstimatedSize = 50 } = props; const { measuredDataMap, lastMeasuredItemIndex } = measuredData; // 如果當(dāng)前索引比已記錄的索引要大,說明要計(jì)算當(dāng)前索引的項(xiàng)的size和offset if (index > lastMeasuredItemIndex) { let offset = 0; // 計(jì)算當(dāng)前能計(jì)算出來的最大offset值 if (lastMeasuredItemIndex >= 0) { const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex]; offset += lastMeasuredItem.offset + lastMeasuredItem.size; } // 計(jì)算直到index為止,所有未計(jì)算過的項(xiàng) for (let i = lastMeasuredItemIndex + 1; i <= index; i++) { const currentItemSize = itemEstimatedSize; measuredDataMap[i] = { size: Number(currentItemSize), offset }; offset += currentItemSize; } // 更新已計(jì)算的項(xiàng)的索引值 // measuredData.lastMeasuredItemIndex = index; } return measuredDataMap[index]; }; const getEndIndex = (props, startIndex) => { const { height, itemCount } = props; // 獲取可視區(qū)內(nèi)開始的項(xiàng) const startItem = getItemMetaData(startIndex); // 可視區(qū)內(nèi)最大的offset值 const maxOffset = Number(startItem.offset) + Number(height); // 開始項(xiàng)的下一項(xiàng)的offset,之后不斷累加此offset,知道等于或超過最大offset,就是找到結(jié)束索引了 let offset = Number(startItem.offset) + startItem.size; // 結(jié)束索引 let endIndex = startIndex; // 累加offset while (offset <= maxOffset && endIndex < (itemCount - 1)) { endIndex++; const currentItem = getItemMetaData(endIndex); offset += currentItem.size; } // 更新已計(jì)算的項(xiàng)的索引值 measuredData.lastMeasuredItemIndex = endIndex; return endIndex; }; const estimatedHeight = (defaultEstimatedItemSize = 50, itemCount) => { let measuredHeight = 0; const { measuredDataMap, lastMeasuredItemIndex } = measuredData; // 計(jì)算已經(jīng)獲取過真實(shí)高度的項(xiàng)的高度之和 if (lastMeasuredItemIndex >= 0) { const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex]; measuredHeight = lastMeasuredItem.offset + lastMeasuredItem.size; } // 未計(jì)算過真實(shí)高度的項(xiàng)數(shù) const unMeasuredItemsCount = itemCount - measuredData.lastMeasuredItemIndex - 1; // 預(yù)測(cè)總高度 totalEstimatedHeight.value = measuredHeight + unMeasuredItemsCount * defaultEstimatedItemSize; } //列表滾動(dòng)處理 const scrollHandle = (event) => { const { scrollTop } = event.currentTarget; scrollOffset.value = scrollTop; getCurrentChildren(); } onMounted(() => { nextTick(() => { getCurrentChildren(); }) }) </script> <style> .hideScrollBar::-webkit-scrollbar { width: 0; } .virtual-wrap { position: relative; overflow: auto; } .virtual-content { position: relative; overflow: auto; } </style>
list組件參數(shù)
props
:定義組件的屬性,包括是否隱藏滾動(dòng)條、容器高度、寬度、每項(xiàng)預(yù)估高度、總項(xiàng)數(shù)、數(shù)據(jù)列表和緩沖區(qū)大小。ref
:定義了一些響應(yīng)式變量,如virtualWrap
、showItemList
、totalEstimatedHeight
和scrollOffset
。watch
:監(jiān)聽props.data
的變化,當(dāng)數(shù)據(jù)變化時(shí)重新計(jì)算可視區(qū)域內(nèi)的項(xiàng)。sizeChangeHandle
:處理列表項(xiàng)大小變化的事件,更新元數(shù)據(jù)。measuredData
:存儲(chǔ)已測(cè)量項(xiàng)的元數(shù)據(jù),包括大小和偏移量。getCurrentChildren
:根據(jù)滾動(dòng)位置計(jì)算當(dāng)前需要渲染的項(xiàng),并更新showItemList
。getRangeToRender
:計(jì)算需要渲染的項(xiàng)的起始和結(jié)束索引。getStartIndex
:根據(jù)滾動(dòng)位置計(jì)算可視區(qū)域開始的項(xiàng)的索引。getItemMetaData
:獲取指定索引項(xiàng)的元數(shù)據(jù),如果未計(jì)算過則進(jìn)行計(jì)算。getEndIndex
:根據(jù)起始索引和緩沖區(qū)大小計(jì)算可視區(qū)域結(jié)束的項(xiàng)的索引。estimatedHeight
:計(jì)算總高度,包括已測(cè)量項(xiàng)和未測(cè)量項(xiàng)。scrollHandle
:處理滾動(dòng)事件,更新滾動(dòng)位置并重新計(jì)算可視區(qū)域內(nèi)的項(xiàng)。onMounted
:組件掛載后,初始化可視區(qū)域內(nèi)的項(xiàng)。
應(yīng)用實(shí)現(xiàn)
<template> <div style="width: 500px; height: 300px"> <List :data="dataList" :itemCount="dataList.length" :height="500" :width="300" :isHideScrollBar="false" @onSizeChange="handleSizeChange" > <template #slot-scope="{ slotProps }"> <!-- 這里定義每個(gè) item 的樣式和內(nèi)容 --> <div class="item-content"> {{ slotProps.data }} </div> </template> </List> </div> </template> <script setup> import List from '@/components/List.vue' import { ref } from 'vue' const dataList = ref([...Array(100).keys()].map((i) => `Item ${i}`)) const handleSizeChange = (index, domNode) => { // 可以在這里處理列表項(xiàng)尺寸變化的邏輯 console.log(`Item ${index} size changed`, domNode) } </script> <style> .item-content { padding: 10px; border-bottom: 1px solid #ddd; background-color: #f9f9f9; } </style>
到此這篇關(guān)于Vue3實(shí)現(xiàn)動(dòng)態(tài)高度的虛擬滾動(dòng)列表的示例代碼的文章就介紹到這了,更多相關(guān)Vue3動(dòng)態(tài)高度虛擬滾動(dòng)列表內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue 監(jiān)聽某個(gè)div垂直滾動(dòng)條下拉到底部的方法
今天小編就為大家分享一篇vue 監(jiān)聽某個(gè)div垂直滾動(dòng)條下拉到底部的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09Vue3.2?setup語(yǔ)法糖及Hook函數(shù)基本使用
這篇文章主要為大家介紹了Vue3.2?setup語(yǔ)法糖及Hook函數(shù)基本使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07vue2.0和mintui-infiniteScroll結(jié)合如何實(shí)現(xiàn)無線滾動(dòng)加載
這篇文章主要介紹了vue2.0和mintui-infiniteScroll結(jié)合如何實(shí)現(xiàn)無線滾動(dòng)加載,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10如何使用el-table實(shí)現(xiàn)純前端導(dǎo)出(適用于el-table任意表格)
我們?nèi)粘W鲰?xiàng)目,特別是后臺(tái)管理系統(tǒng),常常需要導(dǎo)出excel文件,這篇文章主要給大家介紹了關(guān)于如何使用el-table實(shí)現(xiàn)純前端導(dǎo)出的相關(guān)資料,本文適用于el-table任意表格,需要的朋友可以參考下2024-03-03vue使用window.open()跳轉(zhuǎn)頁(yè)面的代碼案例
這篇文章主要介紹了vue中對(duì)window.openner的使用,vue使用window.open()跳轉(zhuǎn)頁(yè)面的代碼案例,本文通過實(shí)例代碼給大家詳細(xì)講解,需要的朋友可以參考下2022-11-11vue自定義橫向滾動(dòng)條css導(dǎo)航兩行排列布局實(shí)現(xiàn)示例
這篇文章主要為大家介紹了vue自定義橫向滾動(dòng)條css導(dǎo)航兩行排列布局實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08在vue中使用inheritAttrs實(shí)現(xiàn)組件的擴(kuò)展性介紹
這篇文章主要介紹了在vue中使用inheritAttrs實(shí)現(xiàn)組件的擴(kuò)展性介紹,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12詳解vuex 中的 state 在組件中如何監(jiān)聽
本篇文章主要介紹了詳解vuex 中的 state 在組件中如何監(jiān)聽,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05