Vue 实现页面到底自动加载无限滚动且不卡方法 home 编辑时间 2020/04/22 ![](/api/file/getImage?fileId=5ea2eaaa16199b501c00911d) ## 前言 其实在我自己写之前,已经在百度调查过 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 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay SpringBoot 项目使用 Jackson 实现解析和序列化 JSON 彻底抛弃FastJson SpringBoot 2 Maven 新建多模块项目配置方法