需求
不知道你是否遇到过这样的需求,甲方爸爸要求将所有的数据都显示出来,还不能分页,后端可是一次性返回3000多条数据啊,按照传统的方式是可以直接显示,那么就意味着你要创建3000多个节点,页面加载得多受影响,虽说现在的电脑算力和浏览器的内核都很强大,但是我们开发中还是要避免这样
解决思路
使用虚拟列表解决。
比如现在要显示10000条数据,那么我们可以在视口(眼睛能够看到的区域)内只显示20条数据,不管你如何滚动视口就显示20条,这样就只需要创建20个元素就行了。
- 固定每条数据的高度,动态计算出父容器的高度
- 监听列表的滚动事件,可以拿到scrollTop,用每条数据的高度除以scrollTop获取到滚动了多少条数据
- 根据滚动了多少条数据,然后切割列表补位已经切割的元素
实现代码
第一步:基本结构的搭建
需要外界传入每条数据的高度,和要显示多少条数据,根据每条数据的高度计算出父容器的高度
这里用到了css3的变量,利用继承,就不用操作列表的属性了 (如果不会,可以直接给每个item动态的设置高度,这样会操作20次属性,不过问题不大)
<template>
<div
class="virtual-list"
:style="{
height: viewPortHeigth,
'--itemHeight': itemHeight + 'px',
}"
>
<div class="scroll-bar" :style="{ height: scrollBarHeight }"></div>
<div class="list" ref="listRef">
<template v-for="item in showList">
<!-- <div class="item" :style={height:itemHeight + 'px'}>{{ item }}</div> -->
<div class="item">{{ item }}</div>
</template>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
// 大数据的列表
list: {
type: Array,
default: [],
},
// 每条数据的高度
itemHeight: {
type: String,
default: '20',
},
// 显示多少条数据
viewCount: {
type: Number,
default: 20,
},
})
const start = ref(0)
const end = ref(props.viewCount)
// 切割大数据列表
const showList = computed(() => {
return props.list.slice(start.value, end.value)
})
// 计算视口的高度
const viewPortHeigth = computed(() => {
return props.viewCount * props.itemHeight + 'px'
})
// 容器的高度是通过每条数据的高度计算的 这会导致无法滚动
// 所以要通过大数据列表的所有数据*每条数据的高度把父容器撑起来
const scrollBarHeight = computed(() => {
return props.list.length * props.itemHeight + 'px'
})
</script>
<style scoped lang="less">
.virtual-list {
position: relative;
border: 1px solid #ccc;
overflow-y: auto;
.list {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.item {
margin: 8px 0;
height: var(--itemHeight);
}
}
</style>
第二步
添加滚动事件,通过scrollTop除以每条数据的高度可以计算出已经滚了多少条数据,然后将滚动到上面遮住的数据从显示列表中去除,去掉几条数据就添加几条新的数据
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop
// 可能会有小数,四舍五入取整
let offset = Math.round(scrollTop / props.itemHeight)
// 这里做个防抖的处理 因为滚动会触发很频繁
if (offset === start.value) return
// offset就是已经滚动多少条
// 比如滚动了1条,那么就要讲切割的列表的第一条给切割掉
// 然后在切割列表的最后添加一条,设置这两个属性 会触发计算属性重新计算
start.value = offset
end.value = start.value + props.viewCount
// 虽然实现了切割效果,但是内容没有往下平移,所以滚动了多少px 就讲列表容器垂直平移多少px
listRef.value.style.transform = `translateY(${scrollTop}px)`
}
完整代码
父组件
<template>
<div class="container">
<virtual-list :list="list" itemHeight="30" :viewCount="20"></virtual-list>
</div>
</template>
<script setup>
import VirtualList from './virtualList.vue'
// 初始化数据
// 这里只是模拟一下数据
// 真实开发中可以监听列表的滚动事件 然后获取数据push到这个列表中
// 当然也可以一次性请求所有数据,这样就不用监听滚动事件了
const list = new Array(10000).fill('').map((itme, index) => ({
index,
size: '100px',
}))
</script>
<style scoped lang="less"></style>
子组件
<template>
<div
class="virtual-list"
@scroll="handleScroll"
:style="{
height: viewPortHeigth,
'--itemHeight': itemHeight + 'px',
}"
>
<div class="scroll-bar" :style="{ height: scrollBarHeight }"></div>
<div class="list" ref="listRef">
<template v-for="item in showList">
<div class="item">{{ item }}</div>
</template>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
list: {
type: Array,
default: [],
},
itemHeight: {
type: String,
default: '20',
},
viewCount: {
type: Number,
default: 20,
},
})
const listRef = ref(null)
const start = ref(0)
const end = ref(props.viewCount)
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop
let offset = Math.round(scrollTop / props.itemHeight)
console.log(offset)
if (offset === start.value) return
start.value = offset
end.value = start.value + props.viewCount
listRef.value.style.transform = `translateY(${scrollTop}px)`
}
const showList = computed(() => {
return props.list.slice(start.value, end.value)
})
const viewPortHeigth = computed(() => {
return props.viewCount * props.itemHeight + 'px'
})
const scrollBarHeight = computed(() => {
return props.list.length * props.itemHeight + 'px'
})
</script>
<style scoped lang="less">
.virtual-list {
position: relative;
border: 1px solid #ccc;
overflow-y: auto;
.list {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.item {
margin: 8px 0;
height: var(--itemHeight);
}
}
</style>