前言

其实在我自己写之前,已经在百度调查过
Vue的第三方库中,有茫茫多的无限加载方案的js可以引入
大可不必重新再造一次轮子

但我个人喜欢,能自己动手,不麻烦别人
所以本文的主旨就是
只用vue.js
循序渐进 共写了4个demo
来实现海量数据,无线滚动加载,且不卡的方法



方案1

基本的思路,用一个array绑定div个
通过v-for,不断扩充array来实现滚动到页尾加载

data.view 用于页面展示绑定dom的array
data.back 用于模拟后端翻页请求的array

每当翻到页面底部,触发翻页
即 增加 data.view 中的数据,dom也会同步变化

缺点:内容如果过多,由于双向绑定的特点,会导致页面出现卡顿。

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>VUE实现无限滚动demo1</title>
  6. <style>
  7. .box {
  8. width: 100%;
  9. text-align: center;
  10. vertical-align: middle;
  11. border: 1px solid #777;
  12. height: 200px;
  13. font-size: 24px;
  14. }
  15. </style>
  16. </head>
  17. <body>
  18. <div id="app">
  19. <div v-for="d in view" class="box">
  20. {{d}}
  21. </div>
  22. </div>
  23. <script src="https://cdn.staticfile.org/vue/2.5.22/vue.js"></script>
  24. <script>
  25. /**
  26. * demo 1
  27. * data.view 用于页面展示绑定dom的array
  28. * data.back 用于模拟后端翻页请求的array
  29. *
  30. * 每当翻到页面底部,触发翻页
  31. * 即 增加 data.view 中的数据,dom也会同步变化
  32. *
  33. * 缺点:内容如果过多,由于双向绑定的特点,会导致页面出现卡顿。
  34. */
  35. let vm = new Vue({
  36. el: '#app',
  37. data: {
  38. size: 10,
  39. page: 0,
  40. view: [],
  41. back: []
  42. },
  43. methods: {
  44. initView: function () {
  45. this.view.push(...this.back.slice(
  46. this.page * this.size,
  47. (this.page + 1) * this.size
  48. ));
  49. }
  50. },
  51. created: function () {
  52. window.onscroll = function () {
  53. const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  54. const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
  55. const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
  56. if (scrollTop + windowHeight >= scrollHeight - windowHeight) {
  57. vm.page++;
  58. vm.initView();
  59. }
  60. };
  61. },
  62. mounted: function () {
  63. for (let i = 0; i < 10000; i++) {
  64. this.back.push(i);
  65. }
  66. this.initView();
  67. }
  68. });
  69. </script>
  70. </body>
  71. </html>

最终效果演示:
https://tczmh.gitee.io/infinitescroll/demo1.html

优点是代码简洁,思路清晰
缺点就引出全文最重要的问题
vue是数据和dom双向绑定的
当数据的数量涨到10000个时
就有10000个线程 在监听数据和dom的双向绑定
早晚要炸内存




方案2

方案2的基础思路是在方案1上改进,要解决dom过多问题
目的是让dom的数量永远保持在一个恒定的数字上
首先先想到的是,底部每增加10条新内容,顶部就减少10条旧内容。

代码如下

  1. initView: function () {
  2. this.view.push(...this.back.slice(
  3. this.page * this.size,
  4. (this.page + 1) * this.size
  5. ));
  6. if(this.page > 1){
  7. this.view.splice(0,this.size)
  8. }
  9. }

但是很快就发现了问题
例如滚动到页尾,触发末尾加载10条,头部减少10条。但滚动条根本没变,视觉上相当于内容自己上移了10行。
在用户看来,就是刚才滚动到19,瞬间变29了。

更严重的是,还在页尾,会连续触发
滚动条只在页尾才会触发加载更多,由于触发完成后,滚动条任然处在页尾,会导致多次重复触发
对于用户而言,就是刚看到第19条,可能瞬间变109条了

解决的思路其实不难
首先,要理解原理,理论上如果你用js/jquery,来操作dom的话,是真的添加和删除dom。
而vue不是,可以理解为,dom其实没变,dom里的数据是自动上移10个身位。
就好比20个人坐20个椅子,我们以为是前10个人搬着椅子走,后10个人搬着椅子来。
但实际上发生的是,椅子根本没变,前10人走,后10人挪10个位置,又来个10个新人坐后10位
所以椅子没变就是解题的关键key

解题思路就在:顶部删除了多少行,用padding补偿多少即可

  1. initView: function () {
  2. console.log(
  3. 'page = ' + this.page
  4. )
  5. this.view.push(...this.back.slice(
  6. this.page * this.size,
  7. (this.page + 1) * this.size
  8. ));
  9. if(this.page > 1){
  10. this.view.splice(0,this.size)
  11. document.querySelector('#app').style.paddingTop = (200 * (this.page - 1) * this.size) + 'px'
  12. }
  13. }

这次可以无感解决移除顶部屏幕外的dom的需求了。
实际顶部一大片都是空白的padding。不会双向绑定消耗资源。

完整代码如下:

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>VUE实现无限滚动demo2</title>
  6. <style>
  7. .box {
  8. width: 100%;
  9. text-align: center;
  10. vertical-align: middle;
  11. border: 1px solid #777;
  12. height: 200px;
  13. font-size: 24px;
  14. }
  15. </style>
  16. </head>
  17. <body>
  18. <div id="app">
  19. <div v-for="d in view" class="box">
  20. {{d}}
  21. </div>
  22. </div>
  23. <script src="https://cdn.staticfile.org/vue/2.5.22/vue.js"></script>
  24. <script>
  25. /**
  26. * demo2
  27. * 在 demo1 上进行改进
  28. * 解决了向下滚动加载时,减少顶部dom
  29. * 并增加顶部padding来撑住滚动条
  30. * 可以实现无感的向下滚动
  31. * 余下需要解决的是,用于如果先滚动到中间,再往上或者往下滚动的情况
  32. */
  33. let vm = new Vue({
  34. el: '#app',
  35. data: {
  36. size: 10,
  37. page: 0,
  38. view: [],
  39. back: []
  40. },
  41. methods: {
  42. initView: function () {
  43. console.log(
  44. 'page = ' + this.page
  45. )
  46. this.view.push(...this.back.slice(
  47. this.page * this.size,
  48. (this.page + 1) * this.size
  49. ));
  50. if(this.page > 1){
  51. this.view.splice(0,this.size)
  52. document.querySelector('#app').style.paddingTop = (200 * (this.page - 1) * this.size) + 'px'
  53. }
  54. }
  55. },
  56. created: function () {
  57. window.onscroll = function () {
  58. const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  59. const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
  60. const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
  61. console.log(
  62. 'scrollTop = ' + scrollTop + ' windowHeight = ' + windowHeight + ' scrollHeight = ' + scrollHeight
  63. );
  64. if (scrollTop + windowHeight >= scrollHeight - windowHeight) {
  65. vm.page++;
  66. vm.initView();
  67. }
  68. };
  69. },
  70. mounted: function () {
  71. for (let i = 0; i < 10000; i++) {
  72. this.back.push(i);
  73. }
  74. this.initView();
  75. }
  76. });
  77. </script>
  78. </body>
  79. </html>

最终效果演示:
https://tczmh.gitee.io/infinitescroll/demo2.html

到这一步肯定还是不完美的,用户只要网上翻两页,就能看到一大片的白色填充区域是没有内容的,就穿帮了。




方案3

继续在方案2上改进,顺着这个思路往下写,就要解决2个问题
首先顶部如果存在padding空白区,当空白区有可能即将要出现在屏幕内时,触发顶部加载function

例如:
当前所在页码是6
顶部缺少的是0 ~ 39 共4页
那判断的参数就是
页码 * 个数 * 高度 (4 * 10 * 200 = 4000)
对比 scrollTop 滚动条距离顶部距离
前者小于等于后者,说明白色区域即将进入屏幕
此时触发向上滚动function
即 增加顶部10条数据,减少顶部10行padding,删除底部10条数据

完整代码如下

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>VUE实现无限滚动demo3</title>
  6. <style>
  7. .box {
  8. width: 100%;
  9. text-align: center;
  10. vertical-align: middle;
  11. border: 1px solid #777;
  12. height: 200px;
  13. font-size: 24px;
  14. }
  15. </style>
  16. </head>
  17. <body>
  18. <div id="app">
  19. <div v-for="d in view" class="box">
  20. {{d}}
  21. </div>
  22. </div>
  23. <script src="https://cdn.staticfile.org/vue/2.5.22/vue.js"></script>
  24. <script>
  25. /**
  26. * demo3
  27. * 在demo2的基础上改进
  28. * 但需要做较大幅度的调整
  29. * 先说思路
  30. * 核心要解决的是在中间,上下都有可能有空白padding的问题
  31. */
  32. let vm = new Vue({
  33. el: '#app',
  34. data: {
  35. // 单页个数
  36. size: 10,
  37. // 当前页码
  38. page: 0,
  39. // 历史最大翻到过页码
  40. maxPage: 1,
  41. // 历史最大滚动条与顶部距离
  42. lastScrollTop: 0,
  43. // 后台总共的页码
  44. totalPage: 10,
  45. // 展示用array
  46. view: [],
  47. // 模拟后台数据array
  48. back: []
  49. },
  50. methods: {
  51. /**
  52. * @param direction 0 上 1 下
  53. */
  54. initView: function (direction) {
  55. switch (direction) {
  56. case 0: {
  57. console.log("===> up")
  58. this.view.unshift(...this.back.slice(
  59. (this.page - 3) * this.size,
  60. (this.page - 2) * this.size
  61. ));
  62. document.querySelector('#app').style.paddingTop = (200 * (this.page - 3) * this.size) + 'px'
  63. this.view.splice(this.view.length - this.size, this.view.length);
  64. this.page--;
  65. break;
  66. }
  67. case 1: {
  68. console.log("===> down " + this.page)
  69. this.view.push(...this.back.slice(
  70. this.page * this.size,
  71. (this.page + 1) * this.size
  72. ));
  73. if (this.page > 1) {
  74. this.view.splice(0, this.size)
  75. document.querySelector('#app').style.paddingTop = (200 * (this.page - 1) * this.size) + 'px'
  76. }
  77. this.page++;
  78. if (this.maxPage < this.page) {
  79. this.maxPage = this.page;
  80. }
  81. break;
  82. }
  83. }
  84. }
  85. },
  86. created: function () {
  87. window.onscroll = function () {
  88. // 滚动条距离顶部
  89. const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  90. // 屏幕显示区域高度
  91. const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
  92. // 滚动条总长
  93. const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
  94. if (vm.lastScrollTop - scrollTop > 0) {
  95. // 向上滚动
  96. // 判断白色padding即将进入屏幕才加载更多
  97. // 首先是从page = 2以后才减少底部dom,所以差是2
  98. // 如果page=6,即 缺少4 * 10 * 200px的空白区
  99. // 若滚动条距顶部小于等于空白区 就是需要触发initview的时间 (预留windowHeight来反应)
  100. console.log(((vm.page - 2) * vm.size * 200 + windowHeight) + '>=' + scrollTop)
  101. if (vm.page > 2 && ((vm.page - 2) * vm.size * 200 + windowHeight) >= scrollTop) {
  102. vm.initView(0);
  103. }
  104. } else {
  105. // 滚动条不含屏幕距离顶部 + 屏幕区域 >= 滚动条总长 可以认为触底
  106. // 但个别浏览器的计算方式有可能ZZ,建议预留部分区域
  107. // 这里的代码把整个 屏幕区域 作为预留
  108. // 也就是 滚动条不含屏幕距离顶部 + 屏幕区域 + 屏幕区域 >= 滚动条总长
  109. // 另外页码不允许大于等于后台总页码
  110. if (vm.page < vm.totalPage && scrollTop + (windowHeight * 2) >= scrollHeight) {
  111. vm.initView(1);
  112. }
  113. }
  114. vm.lastScrollTop = scrollTop;
  115. };
  116. },
  117. mounted: function () {
  118. for (let i = 0; i < this.totalPage * this.size ; i++) {
  119. this.back.push(i);
  120. }
  121. this.initView(1);
  122. }
  123. });
  124. </script>
  125. </body>
  126. </html>

最终效果演示:
https://tczmh.gitee.io/infinitescroll/demo3.html

代码中顺便加入了一些特殊情况的限制,增加了代码稳定性
例如从一开始默认就是只在页码大于等于2的情况下加载更多
故 最多就存在20条数据的dom
那反向上移也是一样,page必须大于2才能触发function
否则会吧page减少到2以下 触发异常情况

再例如加入后台代码总页码数
向下加载一样不允许大于等于后台页码
相当于999页是最后一次被允许加载,1000页就跳过不执行function

两种情况下,如果说都按照 进入屏幕边缘瞬间,才开始加载更多,就来不及了
会出现0.01秒的白边。如果是正式环境,考虑到网络因素,还会更慢
故 2边统一用屏幕高度 windowHeight 来当作缓冲




方案4

理论上来讲方案3已经解决问题了,但在我看来还有一个小小瑕疵
就是向上滚动的时候,底部内容会逐渐减少,导致滚动条缩短
如果前提是我们知道是一种”假的”无限加载方案
那最多觉得,虽然有点不符合常理,只要不影响使用就好
但假如提前不知道是”假的”无限滚动加载方案的话
那就会穿帮!!!
所以最终要实现的,是用户看起来和demo1一模一样,但实际只有20个真实的dom,其他用户看不到的地方,都是白色padding。
所以方案4就是在方案3的基础上,增加底部padding