Vue框架下的虚拟列表

原生虚拟列表

固定列表高度的实现

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <style>
  7. .list-wrap{
  8. position: relative;
  9. overflow-y: scroll;
  10. width: 200px;
  11. margin: 100px auto;
  12. box-sizing: border-box;
  13. border: solid 1px red;
  14. }
  15. .list{
  16. position: absolute;
  17. top: 0;
  18. left: 0;
  19. }
  20. .list li{
  21. border: solid 1px blue;
  22. line-height: 30px;
  23. }
  24. </style>
  25. </head>
  26. <body>
  27. <ul id="app">
  28. <div class="list-wrap" ref="listWrap" @scroll="scrollListener">
  29. <!-- 利用scroll-bar将盒子的高度撑开 -->
  30. <div class="scroll-bar" ref="scrollBar"></div>
  31. <!-- 展示可视化区域的列表 -->
  32. <ul class="list" ref="list">
  33. <li v-for="val in showList">{{val}}</li>
  34. </ul>
  35. </div>
  36. </ul>
  37. <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  38. <script>
  39. new Vue({
  40. el: '#app',
  41. data(){
  42. return {
  43. list: [],//超长的显示数据
  44. itemHeight: 30,//每一列的高度
  45. showNum: 10,//显示几条数据
  46. start: 0,//滚动过程显示的开始索引
  47. end: 10,//滚动过程显示的结束索引
  48. }
  49. },
  50. computed: {
  51. //显示的数组,用计算属性计算
  52. showList(){
  53. return this.list.slice(this.start, this.end);
  54. }
  55. },
  56. mounted(){
  57. //构造一个超长列表
  58. for (let i = 0; i < 1000000; i++) {
  59. this.list.push('列表' + i)
  60. }
  61. //计算滚动容器高度
  62. this.$refs.listWrap.style.height = this.itemHeight * this.showNum + 'px';
  63. //计算总的数据需要的高度,构造滚动条高度
  64. this.$refs.scrollBar.style.height = this.itemHeight * this.list.length + 'px';
  65. },
  66. methods: {
  67. scrollListener(){
  68. //获取滚动高度
  69. let scrollTop = this.$refs.listWrap.scrollTop;
  70. //开始的数组索引
  71. this.start = Math.floor(scrollTop / this.itemHeight);
  72. //结束索引
  73. this.end = this.start + this.showNum;
  74. //绝对定位对相对定位的偏移量
  75. this.$refs.list.style.top = this.start * this.itemHeight + 'px';
  76. console.log(this.start, this.end)
  77. }
  78. }
  79. })
  80. </script>
  81. </body>
  82. </html>

不固定列表高度的实现

参考:https://juejin.cn/post/6844903982742110216#heading-0

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <style>
  7. html {
  8. height: 100%;
  9. }
  10. body {
  11. height: 100%;
  12. margin: 0;
  13. }
  14. #app {
  15. height: 100%;
  16. }
  17. .infinite-list-container {
  18. overflow: auto;
  19. position: relative;
  20. -webkit-overflow-scrolling: touch;
  21. }
  22. .infinite-list-phantom {
  23. position: absolute;
  24. left: 0;
  25. top: 0;
  26. right: 0;
  27. z-index: -1;
  28. }
  29. .infinite-list {
  30. left: 0;
  31. right: 0;
  32. top: 0;
  33. position: absolute;
  34. }
  35. .infinite-list-item {
  36. padding: 5px;
  37. color: #555;
  38. box-sizing: border-box;
  39. border-bottom: 1px solid #999;
  40. /* height:200px; */
  41. }
  42. </style>
  43. </head>
  44. <body>
  45. <div id="app">
  46. <virtual-list :listdata="data" :estimateditemsize='85' v-slot="slotProps">
  47. <item :item="slotProps.item" />
  48. </virtual-list>
  49. </div>
  50. <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  51. <script>
  52. // 全局组件
  53. Vue.component('virtual-list', {
  54. props: {
  55. //所有列表数据
  56. listdata: {
  57. type: Array,
  58. default: () => []
  59. },
  60. //预估高度
  61. estimateditemsize: {
  62. type: Number,
  63. required: true
  64. },
  65. //容器高度 100px or 50vh
  66. height: {
  67. type: String,
  68. default: "100%"
  69. }
  70. },
  71. data() {
  72. return {
  73. //可视区域高度
  74. screenHeight: 0,
  75. //起始索引
  76. start: 0,
  77. //结束索引
  78. end: 0
  79. };
  80. },
  81. computed: {
  82. // 所有的列表数据
  83. _listData() {
  84. return this.listdata.map((item, index) => {
  85. return {
  86. _index: `_${index}`,
  87. item
  88. };
  89. });
  90. },
  91. // 可视区域可以渲染数据的条数
  92. visibleCount() {
  93. return Math.ceil(this.screenHeight / this.estimateditemsize);
  94. },
  95. // 可视区域渲染的数据
  96. visibleData() {
  97. return this._listData.slice(this.start, this.end);
  98. }
  99. },
  100. created() {
  101. this.initPositions();
  102. },
  103. mounted() {
  104. this.screenHeight = this.$el.clientHeight;
  105. this.start = 0;
  106. this.end = this.start + this.visibleCount;
  107. },
  108. updated() {
  109. this.$nextTick(function () {
  110. if (!this.$refs.items || !this.$refs.items.length) {
  111. // 没有已经渲染的数据则不进行下面的操作
  112. return;
  113. }
  114. //获取真实元素大小,修改对应的尺寸缓存
  115. this.updateItemsSize();
  116. //更新列表总高度
  117. let height = this.positions[this.positions.length - 1].bottom;
  118. this.$refs.phantom.style.height = height + "px";
  119. //更新真实偏移量
  120. this.setStartOffset();
  121. });
  122. },
  123. methods: {
  124. initPositions() {
  125. // 记录每一条数据的相关信息,每一条数据使用预估高度
  126. this.positions = this.listdata.map((d, index) => ({
  127. index,
  128. height: this.estimateditemsize,
  129. top: index * this.estimateditemsize,
  130. bottom: (index + 1) * this.estimateditemsize
  131. }));
  132. },
  133. //获取列表起始索引
  134. getStartIndex(scrollTop = 0) {
  135. //二分法查找
  136. return this.binarySearch(this.positions, scrollTop);
  137. },
  138. //二分法查找
  139. binarySearch(list, value) {
  140. let start = 0;
  141. let end = list.length - 1;
  142. let tempIndex = null;
  143. while (start <= end) {
  144. let midIndex = parseInt((start + end) / 2);
  145. let midValue = list[midIndex].bottom;
  146. if (midValue === value) {
  147. return midIndex + 1;
  148. } else if (midValue < value) {
  149. start = midIndex + 1;
  150. } else if (midValue > value) {
  151. if (tempIndex === null || tempIndex > midIndex) {
  152. tempIndex = midIndex;
  153. }
  154. end = end - 1;
  155. }
  156. }
  157. return tempIndex;
  158. },
  159. //获取列表项的当前尺寸
  160. updateItemsSize() {
  161. let nodes = this.$refs.items;
  162. nodes.forEach(node => {
  163. let rect = node.getBoundingClientRect();
  164. let height = rect.height;
  165. let index = +node.id.slice(1);
  166. let oldHeight = this.positions[index].height;
  167. let dValue = oldHeight - height;
  168. //存在差值
  169. if (dValue) {
  170. this.positions[index].bottom = this.positions[index].bottom - dValue;
  171. this.positions[index].height = height;
  172. for (let k = index + 1; k < this.positions.length; k++) {
  173. this.positions[k].top = this.positions[k - 1].bottom;
  174. this.positions[k].bottom = this.positions[k].bottom - dValue;
  175. }
  176. }
  177. });
  178. },
  179. //获取当前的偏移量
  180. setStartOffset() {
  181. let startOffset =
  182. this.start >= 1 ? this.positions[this.start - 1].bottom : 0;
  183. this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;
  184. },
  185. //滚动事件
  186. scrollEvent() {
  187. //当前滚动位置
  188. let scrollTop = this.$refs.list.scrollTop;
  189. //此时的开始索引
  190. this.start = this.getStartIndex(scrollTop);
  191. //此时的结束索引
  192. this.end = this.start + this.visibleCount;
  193. //此时的偏移量
  194. this.setStartOffset();
  195. }
  196. },
  197. template: `<div ref="list" :style="{height}" class="infinite-list-container" @scroll="scrollEvent($event)">
  198. <div ref="phantom" class="infinite-list-phantom"></div>
  199. <div ref="content" class="infinite-list">
  200. <div
  201. class="infinite-list-item"
  202. ref="items"
  203. :id="item._index"
  204. :key="item._index"
  205. v-for="item in visibleData"
  206. >
  207. <slot ref="slot" :item="item.item"></slot>
  208. </div>
  209. </div>
  210. </div>`
  211. })
  212. Vue.component('item', {
  213. props: {
  214. //所有列表数据
  215. item: {
  216. type: Object,
  217. default: () => {}
  218. }
  219. },
  220. template: `
  221. <p>
  222. <span style="color:red">{{item.id}}</span>
  223. {{item.value}}
  224. </p>
  225. `
  226. })
  227. new Vue({
  228. el: '#app',
  229. data() {
  230. let data = [];
  231. for (let id = 0; id < 1000; id++) {
  232. data.push({
  233. id,
  234. value: 'Earum illum esse dolor. Vel reiciendis nisi sit qui ipsam et. Ea at nihil officiis odio. Iste voluptate earum nobis non architecto id corporis ea. Est ullam repellendus dolor fugit numquam veniam omnis.' // 长文本
  235. });
  236. }
  237. return {
  238. // 虚拟滚动的所有列表数据
  239. data
  240. }
  241. },
  242. mounted() {}
  243. })
  244. </script>
  245. </body>
  246. </html>

React框架下的虚拟列表