思路
- 监听列表元素的滚动事件,滚动到底部的时候,加载下一页的数据
- 监听数据加载,判断是否已全部加载结束
实现
监听滚动事件
- 为列表元素 listBox 绑定 scroll 事件进行监听
<div class="listBox" @scroll="watchScroll">
<div class="sinList" v-for="(item, index) in dataList" :key="index">
{{ item.label }}--{{ item.value }}
</div>
<div class="dataDesc">
{{
dataList.length == 0
? "暂无数据"
: loading && !finished
? "加载中"
: "没有更多了"
}}
</div>
</div>
- scrollTop:listBox 滚动时,向上卷曲出去的距离
- clientHeight:listBox 滚动时,可视的高度
- scrollHeight:listBox 的总高度
- 滚动的过程中,scrollTop 不断变大,直至 scrollTop + clientHeight == scrollHeight 时,表示 listBox 已经滚动到了底部
- 滚动到底部时,如果 finished 为 false,表示数据还未加载完,此时需要调用接口,加载下一页的数据
- 加载完数据后,scrollHeight 变大
- 继续滚动,scrollTop 变大,直至再次 scrollTop + clientHeight == scrollHeight …
- 为了优化体验,可以使“调用接口”提前触发,即在距离底部一定距离时,就触发接口加载。
- 此距离取个名字为安全距离 safeHeight,即 scrollTop + clientHeight >= scrollHeight - safeHeight 时,触发接口调用
// 监听滚动事件
watchScroll(e) {
let scrollTop = e.target.scrollTop; // listBox 滚动条向上卷曲出去的长度,随滚动变化
let clientHeight = e.target.clientHeight; // listBox 的视口可见高度,固定不变
let scrollHeight = e.target.scrollHeight; // listBox 的整体高度,随数据加载变化
let saveHeight = 30; // 安全距离,距离底部XX时,触发加载
let tempVal = scrollTop + clientHeight + saveHeight; // 向上卷曲距离 + 视口可见高度 + 安全距离
console.log(
"scrollTop:" +
scrollTop +
";clientHeight:" +
clientHeight +
";scrollHeight:" +
scrollHeight,
";tempVal:" + tempVal
);
// 如果不加入 saveHeight 安全距离,在 scrollTop + clientHeight == scrollHeight 时,触发加载
// 加入安全距离,相当于在 scrollTop + clientHeight >= scrollHeight - 30 时,触发加载,比前者更早触发
if (tempVal >= scrollHeight) {
console.log("滚动到底了");
if (!this.finished && !this.switch) {
// 数据加载未结束 && 未加锁
this.getData();
}
this.switch = true; // 加锁,防止重复触发
} else {
console.log("还没有滚动到底");
}
},
请求接口数据
- 当 res.data 的条数 < pageSize 的时候,说明数据已经请求完毕
- 当 dataList 的条数 == total 时,说明数据已经请求完毕
- 以上两种情况可以将全局变量 finished 置为 true
- 其他情况视为加载还未结束,将 finished 置为 false,页码自加 1,改变滚动锁的状态
- 滚动锁 switch 用来防止接口被重复请求。当滚动到达底部时,请求接口时,将 switch 置为 true,即使再次触发 tempVal >= scrollHeight 也不再执行数据请求。
- 直到请求到数据,并且判断数据还未加载完,则将 switch 置为 false,可以进入下次滚动判断
getData() {
let params = {
pageSize: this.pageSize,
pageNum: this.pageNum,
};
this.loading = true;
this.mockList(params).then((res) => {
this.loading = false;
if (res.code == 200) {
if (res.data && res.data.length > 0) {
this.dataList = this.dataList.concat(res.data);
this.total = res.totalCount;
if (
res.data.length < this.pageSize ||
this.dataList.length >= this.total
) {
// 返回结果条数少于请求条数,认为已结束
// 目前数据条数等于总条数,认为已结束
this.finished = true;
} else {
this.finished = false;
this.pageNum += 1; // 页码加1
this.switch = false; // 还可以继续加载,改变锁状态
}
}
}
});
},
图示
完整代码
<template>
<div class="page">
<div class="listBox" @scroll="watchScroll">
<div class="sinList" v-for="(item, index) in dataList" :key="index">
{{ item.label }}--{{ item.value }}
</div>
<div class="dataDesc">
{{
dataList.length == 0
? "暂无数据"
: loading && !finished
? "加载中"
: "没有更多了"
}}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
pageSize: 10,
pageNum: 1,
total: 0,
loading: false,
finished: true,
switch: false, // 加锁,防止滚动时,判断条件重复调用
dataList: [],
};
},
mounted() {
this.getData();
},
methods: {
getData() {
let params = {
pageSize: this.pageSize,
pageNum: this.pageNum,
};
this.loading = true;
this.mockList(params).then((res) => {
this.loading = false;
if (res.code == 200) {
if (res.data && res.data.length > 0) {
this.dataList = this.dataList.concat(res.data);
this.total = res.totalCount;
if (
res.data.length < this.pageSize ||
this.dataList.length >= this.total
) {
// 返回结果条数少于请求条数,认为已结束
// 目前数据条数等于总条数,认为已结束
this.finished = true;
} else {
this.finished = false;
this.pageNum += 1; // 页码加1
this.switch = false; // 还可以继续加载,改变锁状态
}
}
}
});
},
// 模拟请求接口
mockList(params) {
console.log("请求接口 params", params);
return new Promise((resolve, reject) => {
let data = [];
for (let i = 0; i < 58; i++) {
data.push({
value: "value" + i,
label: "label" + i,
});
}
// 模拟分页
let tempArr = data.splice(
params.pageSize * (params.pageNum - 1),
params.pageSize * params.pageNum
);
let res = {
data: tempArr,
totalCount: data.length,
pageSize: params.pageSize,
pageNum: params.pageNum,
code: 200,
msg: "success",
};
setTimeout(() => {
resolve(res);
}, 100);
});
},
// 监听滚动事件
watchScroll(e) {
let scrollTop = e.target.scrollTop; // listBox 滚动条向上卷曲出去的长度,随滚动变化
let clientHeight = e.target.clientHeight; // listBox 的视口可见高度,固定不变
let scrollHeight = e.target.scrollHeight; // listBox 的整体高度,随数据加载变化
let saveHeight = 30; // 安全距离,距离底部XX时,触发加载
let tempVal = scrollTop + clientHeight + saveHeight; // 向上卷曲距离 + 视口可见高度 + 安全距离
console.log(
"scrollTop:" +
scrollTop +
";clientHeight:" +
clientHeight +
";scrollHeight:" +
scrollHeight,
";tempVal:" + tempVal
);
// 如果不加入 saveHeight 安全距离,在 scrollTop + clientHeight == scrollHeight 时,触发加载
// 加入安全距离,相当于在 scrollTop + clientHeight >= scrollHeight - 30 时,触发加载,比前者更早触发
if (tempVal >= scrollHeight) {
console.log("滚动到底了");
if (!this.finished && !this.switch) {
// 数据加载未结束 && 未加锁
this.getData();
}
this.switch = true; // 加锁,防止重复触发
} else {
console.log("还没有滚动到底");
}
},
},
};
</script>
<style lang="scss" scoped>
.page {
width: 100%;
height: 100vh;
background: #f1f1f1;
.listBox {
background: #fff;
width: 600px;
height: 300px;
overflow: auto;
padding: 20px;
box-sizing: border-box;
.sinList {
height: 40px;
line-height: 40px;
}
.dataDesc {
color: #999;
text-align: center;
}
}
}
</style>