Vue 实现页面滚动到底 自动加载无限滚动 方法思路
2020-06-10
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
## 前言
其实在我自己写之前,已经在百度调查过
Vue的第三方库中,有茫茫多的无限加载方案的js可以引入
大可不必重新再造一次轮子
但我个人喜欢,能自己动手,不麻烦别人
所以本文的主旨就是
**只用vue.js**
**循序渐进 共写了4个demo**
**来实现海量数据,无线滚动加载,且不卡的方法**
## 方案1
**基本的思路,用一个array绑定div个**
**通过v-for,不断扩充array来实现滚动到页尾加载**
`data.view` 用于页面展示绑定dom的array
`data.back` 用于模拟后端翻页请求的array
每当翻到页面底部,触发翻页
即 增加 data.view 中的数据,dom也会同步变化
**缺点:内容如果过多,由于双向绑定的特点,会导致页面出现卡顿。**
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>VUE实现无限滚动demo1</title>
<style>
.box {
width: 100%;
text-align: center;
vertical-align: middle;
border: 1px solid #777;
height: 200px;
font-size: 24px;
}
</style>
</head>
<body>
<div id="app">
<div v-for="d in view" class="box">
{{d}}
</div>
</div>
<script src="https://cdn.staticfile.org/vue/2.5.22/vue.js"></script>
<script>
/**
* demo 1
* data.view 用于页面展示绑定dom的array
* data.back 用于模拟后端翻页请求的array
*
* 每当翻到页面底部,触发翻页
* 即 增加 data.view 中的数据,dom也会同步变化
*
* 缺点:内容如果过多,由于双向绑定的特点,会导致页面出现卡顿。
*/
let vm = new Vue({
el: '#app',
data: {
size: 10,
page: 0,
view: [],
back: []
},
methods: {
initView: function () {
this.view.push(...this.back.slice(
this.page * this.size,
(this.page + 1) * this.size
));
}
},
created: function () {
window.onscroll = function () {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
if (scrollTop + windowHeight >= scrollHeight - windowHeight) {
vm.page++;
vm.initView();
}
};
},
mounted: function () {
for (let i = 0; i < 10000; i++) {
this.back.push(i);
}
this.initView();
}
});
</script>
</body>
</html>
```
最终效果演示:
[https://tczmh.gitee.io/infinitescroll/demo1.html](https://tczmh.gitee.io/infinitescroll/demo1.html)
优点是代码简洁,思路清晰
缺点就引出全文最重要的问题
vue是数据和dom双向绑定的
当数据的数量涨到10000个时
就有10000个线程 在监听数据和dom的双向绑定
早晚要炸内存
<br>
## 方案2
**方案2的基础思路是在方案1上改进,要解决dom过多问题**
目的是让dom的数量永远保持在一个恒定的数字上
首先先想到的是,底部每增加10条新内容,顶部就减少10条旧内容。
代码如下
```javascript
initView: function () {
this.view.push(...this.back.slice(
this.page * this.size,
(this.page + 1) * this.size
));
if(this.page > 1){
this.view.splice(0,this.size)
}
}
```
**但是很快就发现了问题**
例如滚动到页尾,触发末尾加载10条,头部减少10条。但滚动条根本没变,视觉上相当于内容自己上移了10行。
在用户看来,就是刚才滚动到19,瞬间变29了。
<br>
**更严重的是,还在页尾,会连续触发**
滚动条只在页尾才会触发加载更多,由于触发完成后,滚动条任然处在页尾,会导致多次重复触发
对于用户而言,就是刚看到第19条,可能瞬间变109条了
<br>
**解决的思路其实不难**
首先,要理解原理,理论上如果你用js/jquery,来操作dom的话,是真的添加和删除dom。
而vue不是,可以理解为,dom其实没变,dom里的数据是自动上移10个身位。
就好比20个人坐20个椅子,我们以为是前10个人搬着椅子走,后10个人搬着椅子来。
但实际上发生的是,椅子根本没变,前10人走,后10人挪10个位置,又来个10个新人坐后10位
所以椅子没变就是解题的关键key
**解题思路就在:顶部删除了多少行,用padding补偿多少即可**
```javascript
initView: function () {
console.log(
'page = ' + this.page
)
this.view.push(...this.back.slice(
this.page * this.size,
(this.page + 1) * this.size
));
if(this.page > 1){
this.view.splice(0,this.size)
document.querySelector('#app').style.paddingTop = (200 * (this.page - 1) * this.size) + 'px'
}
}
```
这次可以无感解决移除顶部屏幕外的dom的需求了。
实际顶部一大片都是空白的padding。不会双向绑定消耗资源。
完整代码如下:
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>VUE实现无限滚动demo2</title>
<style>
.box {
width: 100%;
text-align: center;
vertical-align: middle;
border: 1px solid #777;
height: 200px;
font-size: 24px;
}
</style>
</head>
<body>
<div id="app">
<div v-for="d in view" class="box">
{{d}}
</div>
</div>
<script src="https://cdn.staticfile.org/vue/2.5.22/vue.js"></script>
<script>
/**
* demo2
* 在 demo1 上进行改进
* 解决了向下滚动加载时,减少顶部dom
* 并增加顶部padding来撑住滚动条
* 可以实现无感的向下滚动
* 余下需要解决的是,用于如果先滚动到中间,再往上或者往下滚动的情况
*/
let vm = new Vue({
el: '#app',
data: {
size: 10,
page: 0,
view: [],
back: []
},
methods: {
initView: function () {
console.log(
'page = ' + this.page
)
this.view.push(...this.back.slice(
this.page * this.size,
(this.page + 1) * this.size
));
if(this.page > 1){
this.view.splice(0,this.size)
document.querySelector('#app').style.paddingTop = (200 * (this.page - 1) * this.size) + 'px'
}
}
},
created: function () {
window.onscroll = function () {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
console.log(
'scrollTop = ' + scrollTop + ' windowHeight = ' + windowHeight + ' scrollHeight = ' + scrollHeight
);
if (scrollTop + windowHeight >= scrollHeight - windowHeight) {
vm.page++;
vm.initView();
}
};
},
mounted: function () {
for (let i = 0; i < 10000; i++) {
this.back.push(i);
}
this.initView();
}
});
</script>
</body>
</html>
```
最终效果演示:
[https://tczmh.gitee.io/infinitescroll/demo2.html](https://tczmh.gitee.io/infinitescroll/demo2.html)
到这一步肯定还是不完美的,用户只要网上翻两页,就能看到一大片的白色填充区域是没有内容的,就穿帮了。
<br>
## 方案3
**继续在方案2上改进,顺着这个思路往下写,就要解决2个问题**
**首先顶部如果存在padding空白区,当空白区有可能即将要出现在屏幕内时,触发顶部加载function**
例如:
当前所在页码是6
顶部缺少的是0 ~ 39 共4页
那判断的参数就是
页码 * 个数 * 高度 (4 * 10 * 200 = 4000)
对比 scrollTop 滚动条距离顶部距离
前者小于等于后者,说明白色区域即将进入屏幕
此时触发向上滚动function
即 增加顶部10条数据,减少顶部10行padding,删除底部10条数据
完整代码如下
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>VUE实现无限滚动demo3</title>
<style>
.box {
width: 100%;
text-align: center;
vertical-align: middle;
border: 1px solid #777;
height: 200px;
font-size: 24px;
}
</style>
</head>
<body>
<div id="app">
<div v-for="d in view" class="box">
{{d}}
</div>
</div>
<script src="https://cdn.staticfile.org/vue/2.5.22/vue.js"></script>
<script>
/**
* demo3
* 在demo2的基础上改进
* 但需要做较大幅度的调整
* 先说思路
* 核心要解决的是在中间,上下都有可能有空白padding的问题
*/
let vm = new Vue({
el: '#app',
data: {
// 单页个数
size: 10,
// 当前页码
page: 0,
// 历史最大翻到过页码
maxPage: 1,
// 历史最大滚动条与顶部距离
lastScrollTop: 0,
// 后台总共的页码
totalPage: 10,
// 展示用array
view: [],
// 模拟后台数据array
back: []
},
methods: {
/**
* @param direction 0 上 1 下
*/
initView: function (direction) {
switch (direction) {
case 0: {
console.log("===> up")
this.view.unshift(...this.back.slice(
(this.page - 3) * this.size,
(this.page - 2) * this.size
));
document.querySelector('#app').style.paddingTop = (200 * (this.page - 3) * this.size) + 'px'
this.view.splice(this.view.length - this.size, this.view.length);
this.page--;
break;
}
case 1: {
console.log("===> down " + this.page)
this.view.push(...this.back.slice(
this.page * this.size,
(this.page + 1) * this.size
));
if (this.page > 1) {
this.view.splice(0, this.size)
document.querySelector('#app').style.paddingTop = (200 * (this.page - 1) * this.size) + 'px'
}
this.page++;
if (this.maxPage < this.page) {
this.maxPage = this.page;
}
break;
}
}
}
},
created: function () {
window.onscroll = function () {
// 滚动条距离顶部
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
// 屏幕显示区域高度
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
// 滚动条总长
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
if (vm.lastScrollTop - scrollTop > 0) {
// 向上滚动
// 判断白色padding即将进入屏幕才加载更多
// 首先是从page = 2以后才减少底部dom,所以差是2
// 如果page=6,即 缺少4 * 10 * 200px的空白区
// 若滚动条距顶部小于等于空白区 就是需要触发initview的时间 (预留windowHeight来反应)
console.log(((vm.page - 2) * vm.size * 200 + windowHeight) + '>=' + scrollTop)
if (vm.page > 2 && ((vm.page - 2) * vm.size * 200 + windowHeight) >= scrollTop) {
vm.initView(0);
}
} else {
// 滚动条不含屏幕距离顶部 + 屏幕区域 >= 滚动条总长 可以认为触底
// 但个别浏览器的计算方式有可能ZZ,建议预留部分区域
// 这里的代码把整个 屏幕区域 作为预留
// 也就是 滚动条不含屏幕距离顶部 + 屏幕区域 + 屏幕区域 >= 滚动条总长
// 另外页码不允许大于等于后台总页码
if (vm.page < vm.totalPage && scrollTop + (windowHeight * 2) >= scrollHeight) {
vm.initView(1);
}
}
vm.lastScrollTop = scrollTop;
};
},
mounted: function () {
for (let i = 0; i < this.totalPage * this.size ; i++) {
this.back.push(i);
}
this.initView(1);
}
});
</script>
</body>
</html>
```
最终效果演示:
[https://tczmh.gitee.io/infinitescroll/demo3.html](https://tczmh.gitee.io/infinitescroll/demo3.html)
代码中顺便加入了一些特殊情况的限制,增加了代码稳定性
例如从一开始默认就是只在页码大于等于2的情况下加载更多
故 最多就存在20条数据的dom
那反向上移也是一样,page必须大于2才能触发function
否则会吧page减少到2以下 触发异常情况
再例如加入后台代码总页码数
向下加载一样不允许大于等于后台页码
相当于999页是最后一次被允许加载,1000页就跳过不执行function
两种情况下,如果说都按照 进入屏幕边缘瞬间,才开始加载更多,就来不及了
会出现0.01秒的白边。如果是正式环境,考虑到网络因素,还会更慢
故 2边统一用屏幕高度 windowHeight 来当作缓冲
<br>
## 方案4
**理论上来讲方案3已经解决问题了,但在我看来还有一个小小瑕疵**
**就是向上滚动的时候,底部内容会逐渐减少,导致滚动条缩短**
如果前提是我们知道是一种"假的"无限加载方案
那最多觉得,虽然有点不符合常理,只要不影响使用就好
但假如提前不知道是"假的"无限滚动加载方案的话
那就会穿帮!!!
所以最终要实现的,是用户看起来和demo1一模一样,但实际只有20个真实的dom,其他用户看不到的地方,都是白色padding。
所以方案4就是在方案3的基础上,增加底部padding