在上文# 内容高度不定的虚拟列表 文章结尾,提到还没实现数据需要不断从后端获取的情况,本文在上文的基础上,做些修改
<template>
<div
ref="list"
:style="{height}"
class="infinite-list-container"
@scroll="scrollEvent"
>
<div
ref="phantom"
class="infinite-list-phantom"
:style="{height: phantomHeight}"
></div>
<div
ref="content"
class="infinite-list"
:style="{transform: `translateY(${contentTransform}px)`}"
>
<div
v-for="item in visibleData"
ref="items"
class="infinite-list-item"
:id="item._index"
:key="item._index"
>
<slot name="content" :item="item.item"></slot>
</div>
</div>
</div>
</template>
<script>
let startIndex = 0; // 新增代码
export default {
name: 'VirtualList',
props: {
// 所有列表数据
listData:{
type: Array,
default: ()=>[]
},
// 预估高度
estimatedItemSize:{
type: Number,
required: true
},
// 缓冲区比例
bufferScale:{
type: Number,
default: 1
},
// 容器高度 100px or 50vh
height: {
type: String,
default: '100%'
},
patchList: {
type: Array
}
},
data() {
return {
// 可视区域高度
screenHeight: 0,
// 起始索引
start: 0,
// 结束索引
end: 0,
phantomHeight: 0,
contentTransform: 0,
};
},
watch: {
patchList() { // 新增代码
this.isLoading = false;
if (startIndex === 0) {
startIndex = this.listData.length;
}
else {
startIndex += this.patchList.length;
}
this.listData.push(...this.patchList);
this.initPositions(this.patchList)
}
},
computed:{
_listData(){
return this.listData.map((item,index)=>{
return {
_index:`_${index}`,
item
}
})
},
visibleCount(){
return Math.ceil(this.screenHeight / this.estimatedItemSize);
},
aboveCount(){
return Math.min(this.start, this.bufferScale * this.visibleCount);
},
belowCount(){
return Math.min(this.listData.length - this.end, this.bufferScale * this.visibleCount);
},
visibleData(){
let start = this.start - this.aboveCount;
let end = this.end + this.belowCount;
return this._listData.slice(start, end);
}
},
created(){
this.initPositions(this.listData);
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
updated(){
this.$nextTick(function () {
if(!this.$refs.items || !this.$refs.items.length){
return;
}
// 获取真实元素大小,修改对应的尺寸缓存
this.updateItemsSize();
// 更新列表总高度
let height = this.positions[this.positions.length - 1].bottom;
this.phantomHeight = `${height}px`;
// 更新真实偏移量
this.setStartOffset();
})
},
methods: {
initPositions(list){
// 此处有变化,上一篇直接遍历listData后返回数据,考虑到数据需要新增,所以这里用push
this.positions.push(...list.map((d,index)=>({
index: index + startIndex,
height: this.estimatedItemSize,
top: (index + startIndex) * this.estimatedItemSize,
bottom: (index+1 + startIndex) * this.estimatedItemSize
})
));
},
//获取列表项的当前尺寸
updateItemsSize(){
let nodes = this.$refs.items;
nodes.forEach((node)=>{
let { height } = node.getBoundingClientRect();
let index = +node.id.slice(1)
this.positions[index].height = height;
})
this.positions[0].bottom = this.positions[0].height;
for (let i = 1, len = this.positions.length; i < len; i++) {
// 为什么加这一句不可以,加上之后,能明显地看到滑到后面之后,渲染内容会往下掉
/* if (this.positions[i].hasUpdated) {
continue;
} */
this.positions[i].top = this.positions[i - 1].bottom;
this.positions[i].bottom = this.positions[i].top + this.positions[i].height;
this.positions[i].hasUpdated = 1;
}
},
// 获取当前的偏移量
setStartOffset(){
// 在渲染时,需要渲染可视区位置以及可视区上下缓冲部分
let index = Math.max(0, this.start - this.aboveCount);
this.contentTransform = this.positions[index].top;
},
//滚动事件
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = this.getStartIndex(scrollTop);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.setStartOffset();
// 此处新增,模拟获取数据
if ((this.listData.length - this.end < 20) && !this.isLoading) {
this.isLoading = true;
this.$emit('fetch-data');
}
},
//获取列表起始索引
getStartIndex(scrollTop = 0) {
let res = this.positions.find(item => item.bottom >= scrollTop);
return res.index;
},
}
};
</script>
<style scoped>
.infinite-list-container {
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.infinite-list-item {
padding: 5px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
/* height:200px; */
}
</style>
测试代码如下:
<template>
<VirtualList
:listData="data"
:estimatedItemSize="100"
:patch-list="newData"
@fetch-data="onFetchData"
>
<template #content="{ item }">
<Item :item="item"/>
</template>
</VirtualList>
</template>
<script>
// 此处的VirtualList为上述代码内容
import VirtualList from '../components/VirtualList.vue'
// Item为每个数据项,可自行实现
import Item from '../components/Item.vue'
import {faker} from '@faker-js/faker';
let globalId = 0;
function mockData (num) {
let data = [];
for (let id = 0; id < num; id++) {
data.push({
id: globalId++,
value: faker.lorem.sentences() // 长文本
})
}
return data;
}
export default {
name: 'virtualExample2',
data(){
return {
data: mockData(20),
newData: []
};
},
components: {
VirtualList,
Item
},
methods: {
onFetchData() {
this.newData = mockData(10);
}
}
}
</script>