基于Vue3实现列表虚拟滚动效果
时间:2022-09-22 10:40:39|栏目:vue|点击: 次
前言
近期在做一个网页播放器项目中,用到很多需要展示歌单的列表
一个歌单动辄千百首歌曲,页面中的元素太多导致热重载的时候 chrome 直接崩了 ?
于是无限滚动列表提上日程
写的有点乱,也是第一次用 typescript 写项目,先记录一下
完成效果
思路和需要解决的问题
与懒加载不同,虚拟滚动需要一次性获取所有数据,但是只显示屏幕可见范围内的数据
要做到这些我需要知道:
- 一行的高度
- 屏幕范围内能显示的行数
- 列表在页面中距离网页顶部的位置
- 滚动条高度
假设满屏能容纳 10 条数据,需要加载的数据是一个数组listData
,只需要裁剪数据范围listData.slice(0, 10)
随着滚动条向下,将滚动条高度/一行的高度可以计算出当前行数
而要模拟滚动条高度就要在页面挂载时就手动设置页面的高度一行高度*listData.length
最后也是最关键的是保持列表一直保持在当前位置上,手动设置列表容器padding-top
等于当前滚动条高度
有一个仍未解决的问题,就是每次来回滚动歌曲封面都要重新请求 ?
vue3+setup 写的组件
<script lang="ts" setup> import { ref, computed, nextTick, reactive, watchEffect, onUnmounted } from 'vue' const props = defineProps<{ listData: Array<any> }>() // 列表HTMLElementDom const ulRef = ref<any>(null) // 屏幕高度 const screenH = document.documentElement.clientHeight const data = reactive<any>({ // 列表第一项的高度(起始高度) initH: 0, // 一行的高度 unitH: 0, // 屏幕范围内能显示个数 displayCount: 1, // 列表起始值 startIdx: 0 }) const listData = computed(() => { let endIdx = data.startIdx + data.displayCount if (endIdx >= props.listData.length) endIdx = props.listData.length return props.listData.slice(data.startIdx, endIdx).map((v, k) => { v.idx = data.startIdx + k + 1 return v }) }) function scrollHandler() { // 当前滚动高度 const curScrollTop = document.documentElement.scrollTop if (curScrollTop > data.initH) { const addCount = Math.floor((curScrollTop - data.initH) / data.unitH) ulRef.value.style.setProperty('padding-top', `${addCount * data.unitH}px`) data.startIdx = addCount } else { ulRef.value.style.setProperty('padding-top', '0px') data.startIdx = 0 } } watchEffect(() => { if (props.listData.length > 0) { nextTick(() => { // 列表距离顶部距离 data.initH = ulRef.value.getBoundingClientRect().top + document.documentElement.scrollTop // 计算每行高度 data.unitH = ulRef.value.children[0].offsetHeight // 计算屏幕内能显示的行数 data.displayCount = Math.ceil(screenH / data.unitH) // 设置列表总高度 = 一行高度 * 行数 const listH = data.unitH * props.listData.length ulRef.value.style.setProperty('height', `${listH}px`) window.removeEventListener('scroll', scrollHandler) window.addEventListener('scroll', scrollHandler) }) } }) onUnmounted(() => { window.removeEventListener('scroll', scrollHandler) }) </script> <template> <ul ref="ulRef"> <li v-for="(listItem, listIndex) in listData" :key="`list-${listIndex}`" :data-idx="listItem.idx"> <slot :listItem="listItem"></slot> </li> </ul> </template>
使用组件
<script lang="ts" setup> import InfiniteList from './InfiniteList.vue' const songs = [] // 列表数据 </script> <template> <infinite-list :listData="songs"> <template #default="{ listItem }"> <div>{{ listItem.title }}</div> <!-- ... --> </template> </infinite-list> </template>