Vue实现虚拟列表无限滚动
要点:
每一项的高度
可视范围内可以展示的数量
列表在页面中距离网页顶部的位置
结构部分
<template>
<div class="virtual-scrolling">
<div class="list" ref="refList" @scroll="scroll">
<div
class="item"
v-for="(item, index) in renderList"
:key="index"
:style="{ transform: `translateY(${translateY}px)` }"
>
<div class="info">
<div class="name">{{ item.name }}</div>
<div class="date">{{ item.date }}</div>
</div>
<div class="desc">{{ item.desc }}</div>
</div>
</div>
</div>
</template>
js部分
const throttle = (fn, delay) => {
let tag = false;
return function (...args) {
if (tag) return;
tag = true;
setTimeout(() => {
fn.apply(this, args);
tag = false;
}, delay);
};
};
export default {
name: "VirtualScrolling",
data() {
return {
list: [],
renderList: [],
itemH: 133,
count: 0,
startIndex: 0,
translateY: 0,
};
},
methods: {
init() {
const clientH = document.documentElement.clientHeight;
this.count = Math.ceil(clientH / this.itemH) + 3;
this.renderList = this.list.splice(this.startIndex, this.count);
},
getPosts() {
return new Promise((resolve, reject) => {
this.$axios.get("/api/posts").then((res) => {
if (res.status === 200) {
this.list = res.data.data;
resolve();
} else {
reject("数据加载失败");
}
});
});
},
scrollHandle() {
const scrollTop = this.$refs.refList.scrollTop;
const start = Math.floor(scrollTop / this.itemH);
// 1
if (this.startIndex != start) {
// 2
const listOffsetTop = scrollTop - (scrollTop % this.itemH);
// 3
this.renderList = this.list.slice(start, this.startIndex + this.count);
// 4
this.translateY = listOffsetTop;
}
this.startIndex = start;
},
scroll() {
throttle(this.scrollHandle(), 50);
},
},
created() {
this.getPosts().then(() => {
this.init();
});
},
};
css部分
.virtual-scrolling {
.list {
height: 100vh;
overflow-y: scroll;
.item {
height: 133px;
box-sizing: border-box;
padding: 10px;
border-bottom: 1px solid #e3e3e3;
.info {
display: flex;
align-items: center;
.name {
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 40px;
background-color: #bbb;
color: #fff;
}
.date {
flex: 1;
margin-left: 10px;
color: #666;
text-align: right;
}
}
.desc {
margin-top: 10px;
}
}
}
}
代码核心方法 scrollHandle (1、2、3、4标记处)
- 起始点不同于上一次,重新渲染列表;
- 需要获得一个可以被itemHeight整除的数来作为item的偏移量;
- 截取需要渲染的那一部分;
- 每一项的位移量 translateY。
最后,使用Vue3完成同样的效果(结构和css部分一样,在此忽略)
import axios from "axios";
import { onMounted, ref } from "vue";
const throttle = function (fn: void, delay: number) {
let tag = false;
return function (...args: []) {
if (tag) return;
tag = true;
setTimeout(function () {
(fn as any).apply(fn, args);
tag = false;
}, delay);
};
};
const refList = ref<HTMLElement | any>();
const list = ref<Array<any>>([]);
const renderList = ref<Array<any>>([]);
const itemH = ref<number>(133);
const count = ref<number>(0);
const startIndex = ref<number>(0);
const translateY = ref<number>(0);
const getPosts = async () => {
const res = await axios.get("/api/posts");
list.value = res.data.data;
init();
};
const scrollHandle = () => {
const scrollTop = refList.value.scrollTop;
const start = Math.floor(scrollTop / itemH.value);
if (startIndex.value != start) {
const listOffsetTop = scrollTop - (scrollTop % itemH.value);
renderList.value = list.value.slice(start, startIndex.value + count.value);
translateY.value = listOffsetTop;
}
startIndex.value = start;
};
const scroll = () => {
throttle(scrollHandle(), 50);
};
const init = () => {
const clientH = document.documentElement.clientHeight;
count.value = Math.ceil(clientH / itemH.value) + 3;
renderList.value = list.value.splice(startIndex.value, count.value);
};
onMounted(() => {
getPosts();
});