Skip to content
On this page

封装虚拟列表组件

TIP

封装的虚拟列表只支持固定高度的列表(每一项的高度固定)。封装时使用了作用域插槽,所有uni-app 小程序端是不支持的

vue-virtual-scroller插件

TIP

其他人封装的虚拟列表插件,支持高度不固定的列表

vue-virtual-scroller

uni-app版本

vue
<!--
 * @Author: 李金深 2896583081@qq.com
 * @Date: 2023-05-16 09:59:08
 * @LastEditors: 李金深 2896583081@qq.com
 * @LastEditTime: 2023-05-16 15:17:54
 * @FilePath: /com.huizhouyiren.APP.d0101/src/components/virtualList/virtualList.vue
 * @Description: 虚拟列表组件
-->
<template>
    <scroll-view scroll-y="true" :style="{ height: contentHeight + 'px' }" @scroll="scrollEvent">
        <view class="relative" :style="{ height: scrollHeight + 'px' }">
            <view :style="{ 'top': `${top}px` }" style="width: 100%;position:absolute" class="left-0 padding_box">
                <view class="flex items-center w-full border_bottom" v-for="(item, index) in showList"
                    :key="item.id + '' + index" :style="{ height: itemHeight + 'px' }">
                    <slot :item="item"></slot>
                </view>
            </view>
        </view>
    </scroll-view>
</template>
<script>
export default {
    props: {
        contentHeight: { // 可视区域高度
            require: true,
            default: 0
        },
        itemHeight: { // 每条数据的高度
            require: true,
            default: 100
        },
        showNum: {  //每次加载到可视区域的数量,itemHeight X showNum 要可视区域高度 ,否则页面滚动不了。
            require: true,
            default: 12
        },
        sourceData: {  // 源数据数组
            require: true,
            default: [],
        }

    },
    data() {
        return {
            showList: [], // 可视区域显示的数据
            top: 0, //偏移量
            scrollTop: 0, //卷起的高度
            startIndex: 0, //可视区域第一条数据的索引
            endIndex: 0, //可视区域最后一条数据后面那条数据的的索引,因为后面要用slice(start,end)方法取需要的数据,但是slice规定end对应数据不包含在里面	
            scrollHeight: [], // 滚动的内容高度
        }
    },
    created() {
        if (this.showNum * this.itemHeight <= this.contentHeight) {
            uni.showToast({
                title: "虚拟列表可视区域中显示的数据条数太少,列表无法滚动",
                icon: "none",
                duration: 3000
            })
        }
    },
    watch: {
        sourceData: {
            handler: function () {
                this.scrollHeight = this.sourceData.length * this.itemHeight
                this.getShowList()
            },
            immediate: true,
            deep: true
        }
    },
    methods: {
        /**
         * @description: 计算可视区域数据
         * @return {*}
         */
        getShowList() {
            this.startIndex = Math.floor(this.scrollTop / this.itemHeight); //可视区域第一条数据的索引
            this.endIndex = this.startIndex + this.showNum; //可视区域最后一条数据的后面那条数据的索引
            this.showList = this.sourceData.slice(this.startIndex, this
                .endIndex) //可视区域显示的数据,即最后要渲染的数据。实际的数据索引是从this.startIndex到this.endIndex-1
            this.top = this.scrollTop - (this.scrollTop % this
                .itemHeight); //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量,这样随机滑动时第一条数据都是完整显示的
        },
        /**
         * @description: 区域滚动事件
         * @param {*} e
         * @return {*}
         */
        scrollEvent(e) {
            this.$u.throttle(() => {
                this.scrollTop = e.detail.scrollTop
                this.getShowList()
            }, 50)
        },
        /**
         * @description: 设置属性值  top  scrollTop等 供父组件使用
         * @param {*} attribute 属性对象 如 {top:1,scrollTop:10}
         * @return {*}
         */
        setAttribute(attribute) {
            Object.keys(attribute).forEach(key => {
                if (Reflect.has(this, key)) {
                    this[key] = attribute[key]
                }
            })
        }
    }
}
</script>
vue
<!--使用-->
<virtual-list ref="chatVirtual" :contentHeight="contentHeight" :itemHeight="102" :sourceData="chat.list">
    <template #default="{ item }">
        <view class="w-full">
            <!--代码逻辑-->
        </view>
    </template>
</virtual-list>

vue3版本

vue
<!--
 * @Author: 李金深 2896583081@qq.com
 * @Date: 2023-06-25 10:33:19
 * @LastEditors: 李金深 2896583081@qq.com
 * @LastEditTime: 2023-06-27 14:09:35
 * @FilePath: /com.huizhouyiren.web.d0101/src/components/virtualList/virtualList.vue
 * @Description: 虚拟列表组件
-->
<script setup>
import { ref, watch, onMounted } from "vue"
import { throttle } from '@/utils/tools'

const props = defineProps({
    itemHeight: { // 每条数据的高度
        require: true,
        default: 100
    },
    showNum: {  //每次加载到可视区域的数量,itemHeight X showNum 要可视区域高度 ,否则页面滚动不了。
        require: true,
        default: 12
    },
    sourceData: {  // 源数据数组
        require: true,
        default: [],
    }

})

let scrollBox = ref(null)

let showList = ref([]) // 可视区域显示的数据
let top = ref(0) //偏移量
let scrollTop = ref(0) //卷起的高度
let startIndex = ref(0)//可视区域第一条数据的索引
let endIndex = ref(0) //可视区域最后一条数据后面那条数据的的索引,因为后面要用slice(start,end)方法取需要的数据,但是slice规定end对应数据不包含在里面	
let scrollHeight = ref([]) // 滚动的内容高度


/**
 * @description: 计算可视区域数据
 * @return {*}
 */
const getShowList = () => {
    startIndex.value = Math.floor(scrollTop.value / props.itemHeight); //可视区域第一条数据的索引
    endIndex.value = startIndex.value + props.showNum; //可视区域最后一条数据的后面那条数据的索引
    showList.value = props.sourceData.slice(startIndex.value, endIndex.value) //可视区域显示的数据,即最后要渲染的数据。实际的数据索引是从this.startIndex到this.endIndex-1
    top.value = scrollTop.value - (
        scrollTop.value % props.itemHeight); //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量,这样随机滑动时第一条数据都是完整显示的
}

/**
 * @description: 设置滚动高度
 * @param {*} scrollTopValue
 * @return {*}
 */
const setTop = (scrollTopValue) => {
    scrollTop.value = scrollTopValue
    getShowList()
}

defineExpose({ setTop })


watch(() => { return props.sourceData }, () => {
    scrollHeight.value = props.sourceData.length * props.itemHeight
    getShowList()
}, {
    deep: true,
    immediate: true
})

onMounted(() => {
    scrollBox.value.addEventListener('scroll', (e) => {
       // throttle 节流函数
        throttle(() => {
            scrollTop.value = e.target.scrollTop
            getShowList()
        }, 50)
    })
})

</script>
<template>
    <div style="height: 100%;width: 100%;">
        <div class="relative scroll_box" style="height: 100%;width: 100%;overflow-y: scroll;" ref="scrollBox">
            <div :style="{ 'top': `${top}px` }" style="width: 100%;position:absolute" class="left-0">
                <div class="flex items-center w-full" v-for="(item, index) in showList" :key="item.id + '' + index"
                    :style="{ height: itemHeight + 'px' }">
                    <slot :item="item"></slot>
                </div>
            </div>
        </div>
    </div>
</template>
<style>
.scroll_box::-webkit-scrollbar {
    height: 0;
    width: 0;
    color: transparent;
}
</style>
vue
<!--使用-->
<VirtualList ref="friendVirtualList" :itemHeight="friendItemHeight" :showNum="friendNum"
    :sourceData="chatStore.friend.list" v-show="chatStore.friend.list.length > 0">
    <template #default="data">
        <div :style="{ height: friendItemHeight + 'px' }" class="flex items-center w-full"
            @click="selectUser(data.item)">
            <div class="flex items-start w-full px-4 cursor-pointer" style="height:2.5rem;">
                <div class="relative flex items-center pr-2">
                    <CustomImage width="2.5rem" roundedClass="rounded-md" :src="data.item.avatar"
                        bgColor="#323232">
                    </CustomImage>
                </div>
                <div class="flex items-center justify-between flex-1 h-full pr-2">
                    <div class="flex-1 mr-2 text-sm title_desc">{{ data.item.nickname }}</div>
                    <SelectBox width="18px" v-if="multiple" v-model:select="data.item.isSelect"
                        :id="data.item.id"></SelectBox>
                </div>
            </div>
        </div>
    </template>
</VirtualList>