当前位置:   article > 正文

vue3 实现虚拟列表_vue3 虚拟列表

vue3 虚拟列表

虚拟列表:按需显示的一种实现,即只对可视区域进行渲染,对非可见区域中的数据不渲染或者部分渲染。

虚拟列表原理:

       仅加载可视区域内的列表项,当发生滚动时计算应展示列表项的startIndex,endIndex,并计算对应的数据在整个列表中的偏移量(为了让列表项展示在可视区域内)

获取元素的scrollTop,然后依此计算在可视区域内展示的列表数据项的前后索引。

  1. <template>
  2. <div class="main" @scroll="throttles" ref="list">
  3. <div :style="{ height: totalHeight + 'px' }"></div>
  4. <div class="list" :style="{ transform }" ref="itemList">
  5. <el-card v-for="item in visibleData" :key="item.index" class="card">
  6. XXX
  7. </el-card>
  8. </div>
  9. </div>
  10. </template>

定高

  1. const scrollTop: number = list.value.scrollTop
  2. //起始索引 向下取整 将仅一部分可见的列表项也渲染出来
  3. start = Math.floor(scrollTop / itemSize)
  4. //结束索引
  5. end = start + visibleCount > data.length ? data.length : start + visibleCount //防止end越界
  6. const data: any = {
  7. list: [], //请求的数据
  8. length: 0 //总数据长度
  9. }
  10. //可视区域展示条数 向上取整,将仅一部分可见的列表项也包含进去
  11. const visibleCount: number = Math.ceil(visibleHeight / itemSize)
  12. //每一项的高度
  13. const itemSize: number = 220
  14. //可视区域高度
  15. const visibleHeight: number = 700

  1. <template>
  2. <div class="main" @scroll="throttles" ref="list">
  3. <!-- 防止滚动条样式变化,当滚动条没有被隐藏时 -->
  4. <div :style="{ height: totalHeight + 'px' }"></div>
  5. <div class="list" :style="{ transform }" ref="itemList">
  6. <el-card v-for="item in visibleData" :key="item.index" class="card">
  7. <el-descriptions :title="'User Info' + item.index">
  8. <el-descriptions-item label="Username">kooriookami</el-descriptions-item>
  9. <el-descriptions-item label="Telephone">18100000000</el-descriptions-item>
  10. <el-descriptions-item label="Place">Suzhou</el-descriptions-item>
  11. <el-descriptions-item label="Remarks">
  12. <el-tag size="small">School</el-tag>
  13. </el-descriptions-item>
  14. <el-descriptions-item label="Address">No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu
  15. Province</el-descriptions-item>
  16. </el-descriptions>
  17. </el-card>
  18. </div>
  19. </div>
  20. </template>
  21. <script lang="ts" setup>
  22. import { ref, onMounted } from 'vue';
  23. const list = ref()
  24. const itemList = ref()
  25. const itemSize: number = 220 //每一项的高度
  26. const visibleHeight: number = 700 //可视区域高度
  27. const visibleCount: number = Math.ceil(visibleHeight / itemSize) //可视区域展示条数
  28. let startOffset: number = 0 //偏移量
  29. let start: number = 0 //开始索引
  30. let end: number = visibleCount - 1 //结束索引
  31. let data: any = {
  32. list: [],//请求的数据
  33. length: 0
  34. }
  35. let totalHeight: number = 0 //总高度
  36. let transform: string = `translate3d(0,${start * itemSize}px,0)` //偏移量
  37. const visibleData: any = ref() //列表展示数据
  38. //模拟请求
  39. const getData = (start: number, end: number) => {
  40. start = start - 10 < 0 ? 0 : start - 10;
  41. if (data.length == 0)
  42. end = end + 10;
  43. else
  44. end = end + 10 > data.length ? data.length : end + 10;
  45. return new Promise((resolve) => {
  46. setTimeout(() => {
  47. let list = []
  48. for (let i = start; i <= end; i++) {
  49. list.push({
  50. index: i,
  51. })
  52. }
  53. resolve({
  54. list,
  55. length: 100
  56. })
  57. }, 30)
  58. })
  59. }
  60. onMounted(async () => {
  61. data = await getData(start, end) //请求的接口
  62. totalHeight = data.length * itemSize //设置真实列表总高度
  63. visibleData.value = data.list //为展示的列表项赋值
  64. })
  65. const scrollEvent = async () => {
  66. const scrollTop: number = list.value.scrollTop
  67. start = Math.floor(scrollTop / itemSize)
  68. end = start + visibleCount > data.length ? data.length : start + visibleCount
  69. data = await getData(start, end)
  70. visibleData.value = data.list
  71. startOffset = start * itemSize
  72. transform = `translate3d(0,${startOffset}px,0)`
  73. }
  74. const throttles = throttle(scrollEvent)
  75. //设置节流
  76. const throttle=(fn: Function, delay: number = 30)=> {
  77. let oldTime = Date.now()
  78. return function () {
  79. const _this: any = this
  80. let newTime = Date.now()
  81. if (newTime - oldTime > delay) {
  82. fn.apply(_this, ...arguments)
  83. oldTime = newTime
  84. }
  85. }
  86. }
  87. </script>
  88. <style lang="scss" scoped>
  89. .main {
  90. overflow-y: scroll;
  91. height: 75vh;
  92. //隐藏滚动条
  93. /* Firefox */
  94. scrollbar-width: none;
  95. /* IE 10+ */
  96. -ms-overflow-style: none;
  97. ::-webkit-scrollbar {
  98. /* Chrome Safari */
  99. display: none;
  100. }
  101. .list {
  102. width: 100%;
  103. .card {
  104. margin-bottom: 20px;
  105. }
  106. }
  107. }
  108. </style>

不定高

不定高初始展示时和定高思路一致,均需要初始化设置一个列表项初始高度,以及真实列表的总高度。

然后在将数据渲染到列表上后获取列表项的真实高度,然后重新设置列表项的高度,偏移量以及真实列表总高度。

  1. <template>
  2. <div class="main" @scroll="throttles" ref="list">
  3. <div :style="{ height: totalHeight + 'px' }"></div>
  4. <div class="list" :style="{ transform }" ref="itemList">
  5. <el-card v-for="item in visibleData" :key="item.index" class="card">
  6. <el-descriptions :title="'User Info' + item.index">
  7. <el-descriptions-item label="Username">kooriookami</el-descriptions-item>
  8. <el-descriptions-item label="Telephone">18100000000</el-descriptions-item>
  9. <el-descriptions-item label="Place">Suzhou</el-descriptions-item>
  10. <el-descriptions-item label="Remarks">
  11. <el-tag size="small">School</el-tag>
  12. </el-descriptions-item>
  13. <el-descriptions-item label="Address">No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu
  14. Province</el-descriptions-item>
  15. </el-descriptions>
  16. //用来模拟不定高的情况
  17. <div style="height: 50px;background: skyblue" v-if="item.hidden">
  18. <span>123</span>
  19. </div>
  20. </el-card>
  21. </div>
  22. </div>
  23. </template>
  24. <script lang="ts" setup>
  25. import { ref, reactive, onMounted, computed, nextTick } from 'vue';
  26. const list = ref()
  27. const itemList = ref()
  28. const itemSize: number = 220 //每一项的高度
  29. const visibleHeight: number = 700 //可视区域高度
  30. const visibleCount: number = Math.ceil(visibleHeight / itemSize) //可视区域展示条数
  31. let startOffset: number = 0 //偏移量
  32. let start: number = 0 //开始索引
  33. let end: number = visibleCount - 1 //结束索引
  34. //模拟请求
  35. const getData = (start: number, end: number) => {
  36. start = start - 10 < 0 ? 0 : start - 10;
  37. if (data.length == 0)
  38. end = end + 10;
  39. else
  40. end = end + 10 > data.length ? data.length : end + 10;
  41. return new Promise((resolve) => {
  42. setTimeout(() => {
  43. let list = []
  44. for (let i = start; i <= end; i++) {
  45. list.push({
  46. index: i,
  47. hidden: i % 2 == 0 ? true : false //控制是否显示与隐藏多的一部分
  48. })
  49. }
  50. resolve({
  51. list,
  52. length: 100
  53. })
  54. }, 30)
  55. })
  56. }
  57. //对数据进行缓存
  58. const getFinalData = (list: any) => {
  59. return list.map((item: any) => {
  60. return {
  61. index: item.index,
  62. hidden: item.hidden,
  63. height: 220,
  64. top: 220 * item.index,
  65. }
  66. })
  67. }
  68. //存储加工好了的数据
  69. const finalData: any = reactive([])
  70. //finalData数组的endIndex
  71. const footer = computed(() => finalData.length - 1)
  72. //更改缓存
  73. const adjustDataList = (list: any) => {
  74. //获取列表项
  75. const documentList: any = itemList.value.children
  76. for (let index = 0; index < list.length; index++) {
  77. //获取列表项的真实高度 +20是因为设置了margin-bottom为20
  78. list[index].height = documentList[index].offsetHeight + 20
  79. }
  80. //列表展示的数据全都没有更新
  81. if (footer.value < start) {
  82. finalData.push(...list)
  83. for (let index = start; index <= end; index++) {
  84. //第一个数据的偏移量为0
  85. if (finalData[index].index == 0)
  86. finalData[index].top = 0
  87. else
  88. //后续数据的偏移量为前一个数据的偏移量加上前一个数据的真实高度
  89. finalData[index].top = finalData[index - 1].top + finalData[index - 1].height
  90. }
  91. }
  92. //列表展示的数据仅一部分更新过,将没更新过的数据过滤出来进行更新后再加入缓存(finalData)
  93. if (footer.value >= start && footer.value < end) {
  94. const data = list.filter((item: any) => item.index > footer.value)
  95. finalData.push(...data)
  96. for (let index = data[0].index; index <= data[data.length - 1].index; index++) {
  97. finalData[index].top = finalData[index - 1].top + finalData[index - 1].height
  98. }
  99. }
  100. //列表项高度发生变化后,真实列表总高度也需要重新计算
  101. totalHeight = finalData[finalData.length - 1].top + finalData[finalData.length - 1].height + (data.length - finalData.length) * itemSize
  102. }
  103. let data: any = {
  104. list: [],//请求的数据
  105. length: 0
  106. }
  107. onMounted(async () => {
  108. data = await getData(start, end)
  109. data.list = getFinalData(data.list.filter((item: any) => item.index >= start && item.index <= end))
  110. totalHeight = data.length * itemSize
  111. visibleData.value = data.list
  112. //vue是异步渲染,需要nextTick在将初始化的列表项渲染完成后再拿到列表项的真实数据然后进行重新渲染
  113. nextTick(() => {
  114. adjustDataList(data.list)
  115. visibleData.value = finalData.slice(start, end + 1)
  116. })
  117. })
  118. let totalHeight: number = 0 //总高度
  119. let transform: string = `translate3d(0,${start * itemSize}px,0)`
  120. const visibleData: any = ref()
  121. const getIndex = (scrollTop: number) => {
  122. //定高
  123. // start = Math.floor(scrollTop / itemSize)
  124. // end = start + visibleCount > data.length ? data.length : start + visibleCount //防止end越界
  125. //不定高 不定高后可视区域展示的数据条数会浮动
  126. start = finalData.findIndex((item: any) => item.top >= scrollTop) - 1
  127. end = finalData.findLastIndex((item: any) => item.top <= scrollTop + visibleHeight) + 1
  128. }
  129. const scrollEvent = async () => {
  130. const scrollTop: number = list.value.scrollTop
  131. getIndex(scrollTop)
  132. data = await getData(start, end)
  133. //前后各多渲染10条数据进行缓冲,防止滑动过快出现空白情况
  134. data.list = getFinalData(data.list.filter((item: any) => item.index >= (start - 10) && item.index <= (end + 10)))
  135. //设置新的偏移量
  136. startOffset = finalData[start].top
  137. transform = `translate3d(0,${startOffset}px,0)`
  138. //如果向上滚动则不需要重新渲染,数据已经加工过并缓存了起来
  139. if (end > footer.value)
  140. visibleData.value = data.list
  141. nextTick(() => {
  142. //有一部分数据没进行过加工才执行
  143. if (end > footer.value)
  144. adjustDataList(data.list)
  145. visibleData.value = finalData.slice(start - 10 < 0 ? 0 : start - 10, end + 10 > footer.value ? footer.value : end + 10)
  146. })
  147. }
  148. const throttles = throttle(scrollEvent)
  149. </script>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/空白诗007/article/detail/998129
推荐阅读
相关标签
  

闽ICP备14008679号