Vue3实现固定高度元素的虚拟列表

249 阅读3分钟

代码只是练习,顺便记录思路,并非封装好的组件,需要根据实际情况调整参数

先尝试想象这样一个场景:

  1. 有一张2m长,10cm宽的纸竖着铺在墙上,这张纸上的字需要透过一种特殊的玻璃才能看到字
  2. 你手里有一个10cm长,10cm宽的特殊玻璃,把它放在纸的最顶部,然后依次往下滑动,就可以看到透过玻璃后面,纸上的文字了,而没有透过玻璃的其他区域,只是白纸没有字
  3. 就这样一路下滑,看完了整张纸
  4. 虚拟列表的原理和上述描述类似,白纸就是被撑起的container,玻璃就是数据渲染区,不透过玻璃的部分不会进行渲染。

理解了上述场景后,虚拟列表的原理可以简单概括为:

  • 指定一个宽高固定的容器container,设置一个容纳数据列表且高度足够高的content撑起container,使container可以滑动(纸的大小)
  • content内部放一个位置随滑动而变化的wrap(玻璃的大小),用来包裹渲染的数据,其位置会随着offsetTop(玻璃距离纸的顶部的距离)的变化而变化
  • 最后设置好当前位置要展示哪些数据即可,根据index计算该位置需要展示哪几条数据

上代码

<!-- HTML部分 -->
<div id="vl-container" class="vf-container" :style="containerStyle" @scroll="onScroll">
    <!-- vl-conetnt用来撑起高度 -->
    <div id="vl-content" class="vf-content" :style="contentStyle">
        <!-- vl-wrap用来定位列表 -->
        <div class="vl-wrap" :style="{ transform: getTransform, fontSize:'28px',color:'#fff' }">
            <div v-for="item in visibleData" :key="item.index" :style="{height: itemHeight+'px', backgroundColor: item.value % 2 === 0? '#345678' : '#123489'}">
                Index{{ item.index }}
            </div>
        </div>
    </div>
</div>
// js部分
import { ref, onMounted, computed } from 'vue'

export default {
    setup(){
        const itemCount = 100; // 数据的总数量
        const itemHeight = 100; // 每一项的高度,固定为100px
        const visibleHeight = 500; // 可视窗口高度
        const visibleWidth = 200; // 可视窗口宽度

        // 列表数据,模拟生成100条数据
        let temp = []
        for(let i=0; i<itemCount; i++){
            temp.push({index:i, value:i+1})
        }
        let List = ref(temp)

        // 外容器container样式
        const containerStyle = {
            position: 'relative',
            width: visibleWidth + 'px',
            height: visibleHeight + 'px',
            overflow: 'auto',
            backgroundColor: '#aefcdd'
        };
        // 100个元素撑起content的实际高度
        const contentStyle = {
            height: itemHeight * itemCount + 'px',
            width: '100%',
        };

        let startOffset = ref(0) // 渲染的数据列表到顶部的距离
        let startIndex = ref(0) // 可视区开始索引,从0开始
        let downBufferEndIndex = ref(null) // 缓冲区结束索引

        // 计算偏移量
        const getTransform = computed(() => {
            return `translate3d(0,${startOffset.value}px,0)`
        })
        // 计算可视化列表项数
        const visibleItemCount = computed(() => {
            return Math.ceil(visibleHeight / itemHeight)
        })
        // 计算虚拟列表数据
        const visibleData = computed(() => {
            // 开始位置-2,为了让顶部缓冲区存在2个列表项
            return List.value.slice(Math.max(0, startIndex.value - 2), Math.min(itemCount, downBufferEndIndex.value))
        })

        const onScroll = (e) => {
            // 当前滚动位置
            const scrollTop = e.target.scrollTop
            // 更新可视区开始索引
            startIndex.value = Math.floor(scrollTop / itemHeight)
            // 更新缓冲区结束索引,+2是为了底部缓冲区存在2个列表项
            downBufferEndIndex.value = startIndex.value + visibleItemCount.value + 2
            //此时的偏移量
            if(startIndex.value - 2 >= 1){ // 因为保留顶部两个缓冲列表项,所以当滑动到第3个元素时,再开始更新位置
                startOffset.value = scrollTop - (scrollTop % itemHeight) - 2*itemHeight;
            }else if(startIndex.value == 0){ // 回到顶部时将位置归零
                startOffset.value = 0
            }
            // 如果不保留顶部,则滑动时直接更新位置
            // startOffset.value = scrollTop - (scrollTop % itemHeight)
        }

        onMounted(() => {
            // 挂载后先更新索引位置,好截取出要渲染的列表项
            startIndex.value = 0;
            downBufferEndIndex.value = startIndex.value + visibleItemCount.value;
        })

        return {
            containerStyle,
            contentStyle,
            itemHeight,
            visibleData,
            getTransform,
            onScroll,
        }
    }