一、适用场景:
1、返回列表数据量大,需要使用滚动条滚动实现动态渲染列表数据
2、返回数据是分页,但是需要通过滚动条实现分页渲染
二、实现方案:
1、原生JavaScript实现
2、Vue3.2实现
以下两个方案亲自验证过,都是一行一行代码敲出来的,非简单CTRL+C+V,描述力求简洁,如有疑问可关注留言
三、方案一:原生JavaScript实现
直接上JavaScript插件代码 virtual-list.js:
/**
*by 郭太东
*只需要传入三个参数,以及重载一个方法setItemElement用来创建自定义的列表元素
*/
class VirtualList {
constructor(options) {
this.options = Object.assign(
{
boxEle: null, // 列表外层的容器DOM元素对象 必选
scrollEle: null, // 列表的容器DOM元素对象 必选
data: [], // 数组列表数据 必选
},
options
);
// 单项占用高度
this.itemH = 0;
this.boxEle = this.options.boxEle;
this.scrollEle = this.options.scrollEle;
this.data = this.options.data;
// div可视窗口最多可显示的项数
this.maxVisibleNum = 0;
// 当前已加载项数
this.curItemNum = 0;
// 为了更平滑的滚动
this.isScrollBusy = false;
this._this = this;
}
/**
* 先渲染一个元素用来拿到每一项的实际数据
*/
getEleInfo() {
const fragment = document.createDocumentFragment();
fragment.appendChild(this.setItemElement(0));
this.scrollEle.append(fragment);
this.itemH =
this.scrollEle.children[0].clientHeight +
Number(
getComputedStyle(this.scrollEle.children[0]).marginBottom.replace(
"px",
""
)
);
this.maxVisibleNum = Math.floor(this.boxEle.clientHeight / this.itemH);
this.curItemNum = this.maxVisibleNum * 2;
}
start() {
if (this.data.length === 0) return;
this.getEleInfo();
// 初始化加载显示 (首次加载,分两种情况)
const fragment = document.createDocumentFragment();
// 第一种:列表总长度小于 this.maxVisibleNum*2
let initNum = this.data.length;
if (this.maxVisibleNum * 2 <= initNum) {
// 第二种:可加载长度大于等于列表的总长度
initNum = this.maxVisibleNum * 2;
// 注册滚动事件
this.boxEle.addEventListener("scroll", this.scrollHandler(this));
}
for (let i = 1; i < initNum; i++) {
fragment.appendChild(this.setItemElement(i));
}
this.scrollEle.append(fragment);
}
scrollHandler(_this) {
return () => {
// 滚动事件发生,动态加载列表
if (_this.isScrollBusy) return;
_this.isScrollBusy = true;
// requestAnimationFrame为了更平滑的滚动, 其实用setTimeout也可以
window.requestAnimationFrame(() => {
_this.isScrollBusy = false;
// 滚动条件:滚动到最新加载列表的三分之二处,开始加载下一页列表
const scrollLine =
_this.scrollEle.clientHeight -
_this.boxEle.clientHeight -
Math.floor(_this.itemH / 3);
const offsetTop = _this.boxEle.scrollTop; // 滚动X轴的偏移量
if (offsetTop >= scrollLine) {
// 开始加载的索引
let sNum = _this.curItemNum;
// 处理列表最后一页的数据
if (_this.data.length > _this.curItemNum + _this.maxVisibleNum) {
_this.curItemNum += _this.maxVisibleNum;
} else {
_this.curItemNum = _this.data.length;
}
const fragment = document.createDocumentFragment();
for (let i = sNum; i < _this.curItemNum; i++) {
fragment.appendChild(_this.setItemElement(i));
}
_this.scrollEle.append(fragment);
}
});
};
}
setItemElement() {
// 创建自定义的DOM元素
}
}
实际使用参考Demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>虚拟列表</title>
</head>
<style>
.virtual-list-box {
width: 300px;
height: 300px;
overflow: auto;
}
.virtual-list-box-item {
margin: 0px 10px 20px 10px;
}
</style>
<body>
<div id="virtual-list-box" class="virtual-list-box">
<div id="virtual-list-scroll" class="virtual-list-scroll"></div>
</div>
</body>
<script src="./virtual-list.js"></script>
<script>
const _json = [];
// 生成测试数据
for (let i = 0; i < 100; i++) {
_json.push({
name: "虚拟列表--" + (i + 1),
});
}
// 创建自定义的列表项DOM节点,重载setItemElement方法
VirtualList.prototype.setItemElement = function (i) {
let listNode = document.createElement("div");
listNode.innerHTML = this.data[i].name;
listNode.className = "virtual-list-box-item";
return listNode;
};
// 传参初始化
new VirtualList({
boxEle: document.querySelector("#virtual-list-box"), // 列表外层的容器DOM元素对象 必选
scrollEle: document.querySelector("#virtual-list-scroll"), // 列表的容器DOM元素对象 必选
data: _json, // 数组列表数据 必选
}).start();
</script>
</html>
四、方案二:Vue3.2实现
虚拟列表组件 VirtualList.vue:
<div ref="boxEle" class="virtual-list-box">
<div ref="scrollEle" class="virtual-list-scroll">
<div v-for="(v, i) in curData" :key="i" class="virtual-list-box-item">
<slot :item="v"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, toRefs } from "vue";
const props = defineProps({
// 列表数据
data: {
type: Array,
default: () => [],
},
// 每行占用高度
itemH: {
type: Number,
default: () => 0,
},
});
const { data, itemH } = toRefs(props);
const boxEle = ref(null); //列表外层的容器DOM元素对象
const scrollEle = ref(null); // 列表的容器DOM元素对象
// div可视窗口最多可显示的项数
let maxVisibleNum = 0;
// 当前已加载项数
let curItemNum = 0;
const curData = ref([]);
// 为了更平滑的滚动
let isScrollBusy = false;
onMounted(() => {
start();
});
function start() {
if (data.value.length === 0) return;
maxVisibleNum = Math.floor(boxEle.value.clientHeight / itemH.value);
curItemNum = maxVisibleNum * 2;
// 初始化加载显示 (首次加载,分两种情况)
// 第一种:列表总长度小于 maxVisibleNum*2
let initNum = data.value.length;
if (maxVisibleNum * 2 <= initNum) {
// 第二种:可加载长度大于等于列表的总长度
initNum = maxVisibleNum * 2;
// 注册滚动事件
boxEle.value.addEventListener("scroll", scrollHandler());
}
curData.value = data.value.slice(0, initNum);
}
function scrollHandler() {
return () => {
// 滚动事件发生,动态加载列表
if (isScrollBusy) return;
isScrollBusy = true;
// requestAnimationFrame为了更平滑的滚动, 其实用setTimeout也可以
window.requestAnimationFrame(() => {
isScrollBusy = false;
// 滚动条件:滚动到最新加载列表的三分之二处,开始加载下一页列表
const scrollLine =
scrollEle.value.clientHeight -
boxEle.value.clientHeight -
Math.floor(itemH.value / 3);
const offsetTop = boxEle.value.scrollTop; // 滚动X轴的偏移量
if (offsetTop >= scrollLine) {
// 处理列表最后一页的数据
if (data.value.length > curItemNum + maxVisibleNum) {
curItemNum += maxVisibleNum;
} else {
curItemNum = data.length;
}
curData.value = data.value.slice(0, curItemNum);
}
});
};
}
</script>
<style lang="less" scoped>
.virtual-list-box {
width: 300px;
height: 300px;
overflow: auto;
}
.virtual-list-box-item {
margin: 0px 10px 20px 10px;
}
</style>
实际使用参考DEMO
<template>
<VirtualList :itemH="40" :data="_json">
<template v-slot="{ item }">
<!-- 此处是列表呈现 -->
<div>{{ item.name }}</div>
</template>
</VirtualList>
</template>
<script setup>
import { ref } from "vue";
import VirtualList from "./VirtualList.vue";
// 生成测试数据
const _json = ref([]);
for (let i = 0; i < 100; i++) {
_json.value.push({
name: "虚拟列表--" + (i + 1),
});
}
</script>
<style lang="less" scoped></style>
以上是Vue的最新写法,[参见](单文件组件 )