欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue3實(shí)現(xiàn)動(dòng)態(tài)高度的虛擬滾動(dòng)列表的示例代碼

 更新時(shí)間:2025年01月10日 09:36:30   作者:偏向明知山  
虛擬滾動(dòng)列表是一種優(yōu)化長(zhǎng)列表渲染性能的技術(shù),通過只渲染可視區(qū)域內(nèi)的列表項(xiàng),減少DOM的渲染數(shù)量,本文就來介紹一下Vue3實(shí)現(xiàn)動(dòng)態(tài)高度的虛擬滾動(dòng)列表的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下

什么是虛擬滾動(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)式變量,如virtualWrapshowItemList、totalEstimatedHeightscrollOffset。

  • 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)文章

最新評(píng)論