起因:
app开发过程中遇到需要渲染3000行的列表,页面直接卡顿,所以开始研究起虚拟列表
实现前提条件:
item项等高列表
实现思路:
首先是dom结构:
- 定义一个容器(固定高度),监听滚动事件
- 容器定义一个内部滚动容器,高度等于列表个数 * 每项高度
- 内部滚动容器里面再定义一个容器,具有transform: translateY属性,根据用户滚动动态改变位置,是真正展示的窗口
- 再里面就是展示的item项列表
然后是实现逻辑:
- 默认展示50条(条数不固定,保证展示能铺满屏幕即可),渲染完成后获取item项高度和最外面容器高度,根据它俩计算出应该展示的item项
Math.ceil(this.containerHeight / this.itemHeight)
- 用户滚动时在滚动事件里根据scrollTop设置translateY属性,用scrollTop % this.itemHeight去掉多余高度,保证会在边界上
translateY = scrollTop - (scrollTop % this.itemHeight)
- 用户滚动时在滚动事件里根据scrollTop和item项高度判断应该展示的起始项index
startIndex = Math.floor(scrollTop / this.itemHeight)
- 最后重新渲染列表item项,渲染
startIndex + Math.ceil(this.boxHeight / this.itemHeight)
之间的item项
代码展示:
<script lang="ts" setup name="virtual-list-comp">
import { onBeforeMount, ref, computed, nextTick, Ref, onMounted } from 'vue'
import { throttle } from '@/util/util'
const props = defineProps({
list: {
type: Array,
default: () => [],
},
})
// 容器高度
const containerHeight = ref(0)
// item项高度(默认取第一个)
const itemHeight = ref(0)
const getHeight = computed(() => {
return itemHeight.value > 0 ? props.list.length * itemHeight.value + 'px' : 'auto'
})
// 真正展示的列表
const showList: Ref<any[]> = ref([])
// 需要渲染的起始项
const startIndex = ref(0)
const itemNum = computed(() => {
if (containerHeight.value && itemHeight.value) {
return Math.ceil(containerHeight.value / itemHeight.value)
}
return 50
})
const translateY = ref(0)
const initShowList = () => {
showList.value = props.list.slice(startIndex.value, startIndex.value + itemNum.value)
}
// 获取盒子高度和item项高度
const initHeight = () => {
containerHeight.value = document.querySelector('.virtual-list')?.clientHeight || 0
itemHeight.value = document.querySelector('.virtual-list__item')?.clientHeight || 0
}
const handleScroll = (e: any) => {
const { scrollTop } = e.target
// 获取 virtual-list__transform 这个展示视口的偏移值
translateY.value = scrollTop - (scrollTop % itemHeight.value)
startIndex.value = Math.floor(scrollTop / itemHeight.value)
initShowList()
}
onBeforeMount(async () => {
initShowList()
await nextTick()
initHeight()
})
onMounted(() => {
document
.querySelector('.virtual-list__container')
?.addEventListener('scroll', throttle(handleScroll, 10))
})
</script>
<template>
<div class="virtual-list h-full">
<div
class="virtual-list__container overflow-y-scroll"
:style="{
height: containerHeight + 'px',
}"
>
<div class="virtual-list__scroll" :style="{ height: getHeight }">
<div class="virtual-list__transform" :style="{ transform: `translateY(${translateY}px)` }">
<div v-for="(item, index) of showList" :key="index" class="virtual-list__item">
<slot name="item" :item="item"></slot>
</div>
</div>
</div>
</div>
</div>
</template>