虚拟列表
作用
优化因渲染大量DOM节点造成的页面卡顿现象
原理
通过js计算的方式,只渲染视口内的dom节点
实现方案
通过监听滚动,仅实时渲染当前视口中,应该展示的数据
通过CSS的translate Y修正当实际渲染数据发生变化时渲染区域与可视区域之间的偏移量
加入缓冲数据,缓解白屏问题
要点
js获取容器/视口的高度:dom.offsetHeight
通过arr.slice(start,end)获取需要随着滚动条滚动而动态展示的数据
通过的css的translate修正渲染区域与可视区域偏移量
通过js获取滚动高度 scrollTop
代码(Vue3)
<template>
<div class="virtual-list-page">
<div class="title">实现一个虚拟列表--item定高</div>
<div class="out-container">
<div class="scroller" ref="scrollerRef" @scroll="onScroll">
<div class="pillar-dom" :style="{height:`${pillarDomHeight}px`}" ></div>
<div class="content-list" :style="styleTranslate">
<div class="item" v-for="item in renderData" :key="item.id">{{ item.data }}</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
itemHeight?:number
}>(),
{
itemHeight:100
}
)
const {itemHeight} = toRefs(props)
// 所有数据
const allData = ref<any[]>([])
// 用于撑开滚动容器的高度
const pillarDomHeight = computed<number>(()=>{
return itemHeight.value * allData.value.length
})
// 内容容器y轴偏移量,当渲染区域第一个元素完全移到了可视区域之外时,需要重新计算startoffset
const startOffset = ref<number>(0)
const styleTranslate = computed<string>(()=>{
return `transform:translate(0,${startOffset.value}px)`
})
const scrollerRef = ref<HTMLDivElement>()
const scrollerRefHeight = computed<number>(()=>{
return scrollerRef.value?scrollerRef.value.offsetHeight:0
})
// 当前视口第一个数据在alldata数组中的索引
const start = ref<number>(0)
// 视口可显示的元素数量
const pageItemCount = computed<number>(()=>{
return Math.ceil(scrollerRefHeight.value / itemHeight.value) + 1
})
// 当前视口最后一个数据在alldata数组中的索引
const end = computed<number>(()=>{
return start.value + pageItemCount.value
})
// 当前视口需要显示的数据
const renderData = computed(()=>{
let realStart = Math.max(0,start.value - pageItemCount.value)
let realEnd = Math.min(end.value + pageItemCount.value,allData.value.length)
return allData.value.slice(realStart,realEnd)
})
// 滚动事件
const onScroll = (e:UIEvent) => {
const scrollDom = e.target as HTMLDivElement
if(!scrollDom) return
const {scrollTop} = scrollDom
start.value = Math.floor(scrollTop / itemHeight.value)
startOffset.value = start.value * itemHeight.value
}
const fetchData = () => {
return new Promise<any[]>(resolve =>{
const list:any[] =[]
for(let i = 0; i < 100000;i++){
list.push({
id:`id-${i}`,
data:i+1
})
}
setTimeout(() => {
resolve(list)
}, 3000);
})
}
const init = async() =>{
allData.value = await fetchData()
}
onMounted(()=>{
init()
})
</script>
<style>
.virtual-list-page{
width: 100%;
height: 100%;
}
.title{
height: 40px;
line-height: 40px;
}
.out-container{
height: calc(100% - 40px);
width: 100%;
}
.scroller{
width: 100%;
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.pillar-dom{
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.content-list{
position: absolute;
top:0;
left: 0;
right: 0;
}
.item{
height: calc(v-bind(itemHeight) * 1px);
line-height: calc(v-bind(itemHeight) * 1px);
border-bottom: 4px solid black;
color:black;
width: 100%;
box-sizing: border-box;
background-color:bisque;
}
.item:last-child{
border-bottom: none;
}
</style>