Vue-封装简易的长列表虚拟滚动组件(含触底加载数据)
封装简易的长列表虚拟滚动组件(含触底加载数据)
虚拟滚动的理解
既可以滚动加载,也不会额外增加DOM数量,随着滚动变化原有几个DOM元素的值
思考
1. 容器该如何布局 ?
2. 如何动态变化可视区域内dom的值 ?
3. 滚动条的长度如何控制?
4. 如何判断上拉触底了 ?
列表容器的布局
1. 新建一个组件ListScroll,容器内部含:滚动条盒子、展示的列表盒子、加载提示盒子
2. 注意最外层的容器viewport高度设置100%,让父组件去决定ListScroll组件的高度
3. 注意让展示的列表盒子绝对定位铺满容器viewport,滚动条的盒子高度需要动态计算
4. 监听容器viewport的滚动,要动态设置列表盒子的translateY属性,让盒子往上移动
动态变化展示数量内的dom元素的值
1. 涉及到的元素的几何属性:容器的viewport的offsetHeight高度,容器的viewport的scrollTop滚动的高度
2. 这个方案需要设置每行数据的高度的 rowHeight
3. 定义startIndex和endIndex,用来截取数据的
4. 通过容器的viewport的scrollTop滚动的高度 / 每行的高度,能获得当前的startIndex
5. 列表展示的数据,是展示源数据通过startIndex和endIndex截取后的数据
滚动条的长度
滚动条的长度 = 源数据的长度 * 每行的高度
判断上拉触底
监听滚动时,当容器的viewport的offsetHeight高度 + 容器的viewport的scrollTop滚动的高度 等于 列表的总长度 则表示触底了
完整代码的实现
ListScroll组件
// components/ListScroll.vue
<template>
<div class="viewport" ref="viewport" @scroll="onScroll">
<!-- 滚动条 -->
<div class="scrollbar" :style="{height: listHeight + 'px'}"></div>
<!-- 展示的列表 -->
<div class="list" ref="list" :style="{ transform: `translateY(${transformOffset}px)` }">
<div class="row" :style="{height: rowHeight + 'px'}" v-for="(item, index) in showList" :key="index">
<slot :record="item"></slot>
</div>
</div>
<!-- 加载 -->
<div class="loading_wrap" v-show="loading">
<div class="loading">
<div class="container"></div>
</div>
<div>正在加载中</div>
</div>
</div>
</template>
<script>
export default {
props: {
// 数据源
list: {
type: Array,
default: () => []
},
// 每行的高度
rowHeight: {
type: Number,
default: 200
},
// 显示数量
viewCount: {
type: Number,
default: 10
},
// 控制loading
loading: {
type: Boolean,
default: false
}
},
data () {
return {
viewHeight: 0, // 可视区域的高度
startIndex: 0, // 开始索引
endIndex: 0, // 结束索引
transformOffset: 0, // 列表的偏移量
isLoading: false // 控制loading
}
},
mounted () {
this.initData()
},
computed: {
// 展示的数据
showList () {
return this.list.slice(this.startIndex, this.endIndex)
},
// 列表的总高度
listHeight () {
return this.list.length * this.rowHeight
}
},
methods: {
// 初始化一些数据
initData () {
this.endIndex = this.viewCount
this.viewHeight = this.$refs.viewport.offsetHeight
},
// 列表滚动
onScroll () {
const scrollTop = this.$refs.viewport.scrollTop // 获取试图往上滚动的高度
const currentIndex = Math.floor(scrollTop / this.rowHeight) // 计算当前的索引
// 只在需要变化的时 才重新赋值
if (this.startIndex !== currentIndex) {
this.startIndex = currentIndex
this.endIndex = this.startIndex + this.viewCount// 结束索引
this.transformOffset = scrollTop - (scrollTop % this.rowHeight)
}
// 触底了
if ((this.viewHeight + scrollTop) === this.listHeight) {
// 发送触底加载事件
this.$emit('bottomLoad')
}
}
}
}
</script>
<style lang="less" scoped>
/*
------最外层容器---------*/
.viewport {
width: 100%;
height: 100%; // 这个的高度让父组件去决定
background-color: #fff;
position: relative;
overflow-y: auto;
}
/*
------列表展示层容器---------*/
.list {
position: absolute;
top: 0;
left: 0;
right: 0;
}
/*
------每行容器---------*/
.row {
overflow: hidden;
}
/*
------loading样式---------*/
.loading_wrap {
display: flex;
justify-content: center;
align-items: center;
color: #999;
padding: 20px 0;
.loading {
box-sizing: border-box;
width: 20px;
height: 20px;
border: 2px solid #ddd;
border-radius: 50%;
animation: rotate 1s linear infinite;
margin-right: 10px;
}
.container {
position: relative;
top: 50%;
left: 50%;
width: 10px;
height: 10px;
background-color: #fff;
}
}
/*
------loading动画---------*/
@keyframes rotate {
from {
transform-origin: center center;
transform: rotate(0deg);
}
to {
transform-origin: center center;
transform: rotate(360deg);
}
}
</style>
页面引入ListScroll组件
<template>
<div class="page">
<h3>长列表渲染</h3>
<ListScroll
class="list_scroll"
:list="listData"
:loading="isLoading"
@bottomLoad="onBottomLoad">
<template v-slot="{ record }">
<div class="row_content" @click="handleClick(record)">
<div>{{ record }}</div>
<img class="image" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F2076f7ae-d134-4dc4-a865-af1b2029d400%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1680249943&t=7646a71b62c810256a2b414e96106808" />
</div>
</template>
</ListScroll>
</div>
</template>
<script>
import ListScroll from '@/components/ListScroll.vue'
export default {
data () {
return {
listData: [], // 总数据
isLoading: false // 展示loading
}
},
mounted () {
this.getListData()
},
components: {
ListScroll
},
methods: {
// 获取数据
getListData () {
const count = 20 + this.listData.length
const start = this.listData.length
this.isLoading = true
setTimeout(() => {
for (let i = start; i < count; i++) {
this.listData.push(i)
}
this.isLoading = false
}, 500)
},
// 监听触底事件
onBottomLoad () {
console.log('触底了')
if (this.listData.length >= 100) {
console.log('数据加载完了~')
return
}
// 加载数据
this.getListData()
},
// 监听点击每行
handleClick (record) {
console.log(record, 'record')
}
}
}
</script>
<style lang="less" scoped>
.page {
display: flex;
flex-direction: column;
height: 100vh;
.list_scroll {
flex: 1;
}
}
.row_content {
width: 100%;
height: 100%;
.image {
display: block;
width: 300px;
height: 160px;
object-fit: cover;
}
}
</style>
总结
个人感觉这种长列表虚拟滚动的方案,也是很适用于平常的列表触底加载业务,以往很多同学都是通过pageSize设置为10来只先渲染10条数据,导致频繁的的下拉会多次发送请求。通过这种方案,我们可以一次性先获取50条的数据,但是控制只在页面展示10个DOM元素,这是不是也巧妙的减少了发送请求获取数据的次数了呢 ?
~~ end ~~
更多推荐
所有评论(0)