实现思路:
1、数据分片: 将要显示的数据单独拿出来,而不是将所有数据一次性加载到页面上。这可以减少页面渲染时的负担
2、可视区域管理: 只渲染用户当前可见的部分内容,即视口中的数据。当用户滚动列表时,动态地加载新的数据块进入视口,并移除离开视口的数据块
3、滚动事件监听: 用户滚动时加载新的渲染数据列表。可以根据滚动位置来计算当前应该显示哪些数据。滚动事件可以加上节流函数优化性能
Vue3 + TS 实例
js
<template>
<div class="container" ref="containerRef">
<ul class="list">
<li v-for="(item, index) in visibleItems" :key="key ? item[key] : index" :style="`height: ${itemHeight}px;`" >
<slot :item="item" :index="index" ></slot>
</li>
<div ref="bottomRef" ><slot name="bottom"></slot></div>
</ul>
<div class="placeholder" :style="`height: ${placeholderHeight}px;`" ></div>
</div>
</template>
<script setup lang="ts">
import { ref, toRefs, computed, onMounted } from 'vue';
const props = defineProps({
/** 列表数据 */
data: {
default: [],
type: Array<any>
},
/** 列表项高度 */
itemHeight: {
default: 50,
type: Number
},
/** 列表项唯一标识字段, 默认index */
key: {
default: '',
type: String
}
})
const { data, itemHeight } = toRefs(props)
const containerRef = ref<HTMLElement | null>(null);
const bottomRef = ref<HTMLElement | null>(null);
const visibleHeight = ref(0) // 可视区域的高度
onMounted(() => {
visibleHeight.value = containerRef.value?.clientHeight || 0
containerRef.value?.addEventListener('scroll', scrollEvent)
})
/** 渲染列表的数量 */
const visibleNum = computed(() => {
return visibleHeight.value / itemHeight.value * 2
})
/** 可视范围起始索引 */
const startIndex = ref(0);
/** 可视范围数据 */
const visibleItems = computed(() => {
return data.value.slice(startIndex.value, startIndex.value + visibleNum.value)
})
/** 整个列表的高度 */
const placeholderHeight = computed(() => {
const bottomRefHeight = bottomRef.value?.clientHeight || 0
const bottomHeight = bottomRefHeight && itemHeight.value > bottomRefHeight ? itemHeight.value : bottomRefHeight
return data.value.length * itemHeight.value - visibleHeight.value + bottomHeight + 7
})
/** 滚动事件 */
const scrollEvent = (event: HTMLElementEventMap['scroll']) => {
if(!event?.target) return
const dom = event.target as HTMLElement
const { scrollTop } = dom
startIndex.value = Math.floor(scrollTop / itemHeight.value)
if(scrollTop <= 0 ) {
return emits('touchTop')
}
if(scrollTop + 100 >= placeholderHeight.value ) {
return emits('touchBottom')
}
}
const emits = defineEmits(['touchTop', 'touchBottom'])
</script>
<style lang="less" scoped>
.container{
width: 100%;
height: 100%;
overflow-y: auto;
position: relative;
&::-webkit-scrollbar {
width: 8px;
background-color: #dfdfdf;
}
&::-webkit-scrollbar-thumb {
background: #999999;
}
&::-webkit-scrollbar-thumb:hover{
background: #707070;
}
}
.list{
width: 100%; height: 100%;
position: sticky;
top: 0; left: 0;
list-style: none;
}
</style>