不同场景下Vue中虚拟列表实现

 更新时间:2023年10月31日 15:32:04   作者:通往自由之路  
虚拟列表用来解决大数据量数据渲染问题,由于一次性渲染性能低,所以诞生了虚拟列表渲染,下面我们就来学习一下不同场景下Vue中虚拟列表是如何实现的吧

虚拟列表用来解决大数据量数据渲染问题,由于一次性渲染性能低,所以诞生了虚拟列表渲染。该场景下可视区高度都是相对固定的。相对固定是指在一定条件下可以被改变。

虚拟列表的要做的事是确保性能的前提下,利用一定的技术模拟全数据一次性渲染后效果。

主要通过两件事:

1,渲染数据,适时变更渲染数据

2,模拟滚动效果

场景一、可视区高度固定,单条数据高度固定且相同

比如element-ui el-select下拉选择框,这是最简单的场景。相关参数固定,也最好实现。

<template>
  <div ref="list" class="render-list-container" @scroll="scrollEvent($event)">
    <div class="render-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    <div class="render-list" :style="{ transform: getTransform }">
      <template
        v-for="item in visibleData"
      >
        <slot :value="item.value" :height="item.height + 'px'"  :index="item.id"></slot>
      </template>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    // 所有列表数据
    listData: {
      type: Array,
      default: () => []
    },
    // 每项高度
    itemSize: {
      type: Number,
      default: 50
    }
  },
  computed: {
    // 列表总高度
    listHeight () {
      return this.listData.length * this.itemSize
    },
    // 可显示的列表项数
    visibleCount () {
      return Math.ceil(this.screenHeight / this.itemSize)
    },
    // 偏移量对应的style
    getTransform () {
      return `translate3d(0,${this.startOffset}px,0)`
    },
    // 获取真实显示列表数据
    visibleData () {
      return this.listData.slice(this.start, Math.min(this.end, this.listData.length))
    }
  },
  mounted () {
    this.screenHeight = this.$el.clientHeight
    this.start = 0
    this.end = this.start + this.visibleCount
  },
  data () {
    return {
      // 可视区域高度
      screenHeight: 0,
      // 偏移量
      startOffset: 0,
      // 起始索引
      start: 0,
      // 结束索引
      end: null
    }
  },
  methods: {
    scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 此时的开始索引
      this.start = Math.floor(scrollTop / this.itemSize)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
      // 此时的偏移量
      this.startOffset = scrollTop - (scrollTop % this.itemSize)
    }
  }
}
</script>

<style scoped>
.render-list-container {
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
  height: 200px;
}

.render-list-phantom {
  position: absolute;
  left: 0;
  right: 0;
  z-index: -1;
}

.render-list {
  text-align: center;
}

</style>

初始化渲染数据

单条数据高度确定,可视区高度确定。可以用二者计算出显示条数。初始索引为0,结束索引为显示条数。之后截取总数据当中对应的数据即可。

 // 可显示的列表项数
    visibleCount () {
      return Math.ceil(this.screenHeight / this.itemSize)
    },
...
  mounted () {
    // 开始索引
    this.start = 0
    // 结束索引
    this.end = this.start + this.visibleCount
  },

滚动逻辑

可视区高度确定,列表需要模拟出滚动条,这里采用占位div撑开方案。随着滚动条滚动,监听滚动高度计算出开始索引和结束索引,再计算出滚动偏移量,再利用translate3d滚动,translate3d且因为没有重排重绘,所以性能更好。

  methods: {
    scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 此时的开始索引
      this.start = Math.floor(scrollTop / this.itemSize)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
      // 此时的偏移量 
      this.startOffset = scrollTop - (scrollTop % this.itemSize)
    }
  }

再渲染逻辑

随着滚动条滚动,监听滚动高度计算出开始索引和结束索引,重置开始和结束索引,也就自然引发Vue重新渲染。

场景二、可视区高度固定,单条数据高度确定但不相同

如果单条数据不固定,一定是因为有不同的数据展示方式,每种方式可以封装成组件,之后动态展示。这块封装了三个组件,高度分别为20px、30px、50px。

封装组件

<template>
    <div style="height:20px;border:1px solid #333;">
        height:20px;---{{ index }}
    </div>
</template>
<script>
export default {
  props: ['index']
}
</script>
<template>
    <div style="height:30px;border:1px solid #333;">
        height: 30px---{{ index }}
    </div>
</template>
<script>
export default {
  props: ['index']
}
</script>
<template>
    <div style="height:50px;border:1px solid #333;">
        height: 50px--{{ index }}
    </div>
</template>
<script>
export default {
  props: ['index']
}
</script>

整体实现

<template>
  <div ref="list" class="render-list-container" @scroll="scrollEvent($event)">
    <div
      class="render-list-phantom"
      :style="{ height: listHeight + 'px' }"
    ></div>
    <div class="render-list" :style="{ transform: getTransform }">
      <template v-for="item in visibleData">
        <slot :type="item.type" :index="item.id"></slot>
      </template>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    // 所有列表数据
    listData: {
      type: Array,
      default: () => []
    }
  },
  computed: {
    // 列表总高度
    listHeight () {
      return this.listData.reduce((acc, curVal) => {
        return acc + curVal.height
      }, 0)
    },
    // 可显示的列表项数
    visibleCount () {
      let accHeight = 0
      let count = 0
      for (let i = 0; i < this.listData.length; i++) {
        accHeight += this.listData[i].height
        if (accHeight >= this.screenHeight) {
          count++
          break
        }
        count++
      }
      return count
    },
    // 偏移量对应的style
    getTransform () {
      return `translate3d(0,${this.startOffset}px,0)`
    },
    // 获取真实显示列表数据
    visibleData () {
      return this.listData.slice(
        this.start,
        Math.min(this.end, this.listData.length)
      )
    }
  },
  mounted () {
    this.screenHeight = this.$el.clientHeight
    this.end = this.start + this.visibleCount
  },
  data () {
    return {
      // 可视区域高度
      screenHeight: 0,
      // 偏移量
      startOffset: 0,
      // 起始索引
      start: 0,
      // 结束索引
      end: null
    }
  },
  methods: {
    getStart (scrollTop) {
      var height = 0
      var start = 0
      var i = 0
      while (true) {
        const currentItem = this.listData[i].height
        if (currentItem) {
          height += currentItem
          if (height >= scrollTop) {
            start = i
            break
          }
        } else {
          break
        }
        i++
      }

      return start
    },
    scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 此时的开始索引
      this.start = this.getStart(scrollTop)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
      const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc + curVal.height, 0) - this.screenHeight)
      // 此时的偏移量
      this.startOffset = offsetHeight < 0 ? 0 : offsetHeight
    }
  }
}
</script>

  <style scoped>
.render-list-container {
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
  height: 200px;
}

.render-list-phantom {
  position: absolute;
  left: 0;
  right: 0;
  z-index: -1;
}

.render-list {
  text-align: center;
}
</style>

初始化渲染逻辑

通过累加组件高度的方式算出初始化显示条数,初始化开始索引为0,结束索引为显示条数

 // 可显示的列表项数
    visibleCount () {
      let accHeight = 0
      let count = 0
      for (let i = 0; i < this.listData.length; i++) {
        accHeight += this.listData[i].height
        if (accHeight >= this.screenHeight) {
          count++
          break
        }
        count++
      }
      return count
    },

滚动逻辑

依然采用占位div撑开方案保证可以滚动。随着滚动条滚动,监听滚动高度,再通过累加方式算出最新的开始索引和结束索引,算出滚动偏移量

  methods: {
    getStart (scrollTop) {
      var height = 0
      var i = 0
      while (true) {
        const currentItem = this.listData[i].height
        height += currentItem
        if (height >= scrollTop) {
            start = ++i
            break
        }
        i++
      }

      return i
    },
    scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 此时的开始索引
      this.start = this.getStart(scrollTop)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
      const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc + curVal.height, 0) - this.screenHeight)
      // 此时的偏移量
      this.startOffset = offsetHeight < 0 ? 0 : offsetHeight
    }
  }

再渲染逻辑

监听滚动高度计算出开始索引和结束索引,重置开始和结束索引,也就自然引发Vue重新渲染。

测试

<template>
<div class="render-show">
  <div>
    <VirtualList :listData="data">
       <template slot-scope="{type, index}">
          <component :is="type" :index="index"></component>
       </template>
    </VirtualList>
  </div>
</div>
</template>

<script>
import VirtualList from './parts/VirtualList'

import Height20 from './parts/Height20'
import Height30 from './parts/Height30'
import Height50 from './parts/Height50'

const d = []
for (let i = 0; i < 1000; i++) {
  const type = i % 3 === 0 ? i % 2 === 0 ? 'Height30' : 'Height50' : 'Height20'
  d.push({ id: i, value: i, type: type, height: type === 'Height30' ? 30 : type === 'Height20' ? 20 : 50 })
}
export default {
  name: 'VirtualList-test',
  data () {
    return {
      data: d
    }
  },
  components: {
    VirtualList,
    Height20,
    Height30,
    Height50
  }
}
</script>

<style>

.render-show {
  display: flex;
  justify-content: center;
}
.render-show > div{
  width:500px;
  margin-top:40px;
}
.render-list-item {
  color: #555;
  box-sizing: border-box;
  border-bottom: 1px solid #999;
  box-sizing: border-box;
}
</style>

场景三、以上两种情况追加数据

以上两种场景。如何追加数据呢?

   scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 此时的开始索引
      this.start = this.getStart(scrollTop)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
      const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc + curVal.height, 0) - this.screenHeight)
      // 此时的偏移量
      this.startOffset = offsetHeight < 0 ? 0 : offsetHeight
      // 追加数据
      if (this.end + 1 >= this.listData.length) {
        this.$emit('appendData', this.listData.length)
      }
    }
    
    ...
    appendData (start) {
      const d = []
      for (let i = start; i < start + 10; i++) {
        const type = i % 3 === 0 ? i % 2 === 0 ? 'Height30' : 'Height50' : 'Height20'
        d.push({ id: i, value: i, type: type, height: type === 'Height30' ? 30 : type === 'Height20' ? 20 : 50 })
      }
      this.data = [...this.data, ...d]
    }

注意事项

以上两个场景中均对偏移量做了处理

      this.startOffset = scrollTop - (scrollTop % this.itemSize)
const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc + curVal.height, 0) - this.screenHeight)
      // 此时的偏移量
      this.startOffset = offsetHeight < 0 ? 0 : offsetHeight

真实的滚动就是滚动条滚动了多少,可视区就向上移动多少。但虚拟滚动不是的。当起始索引发生变化时,渲染数据发生变化了,但渲染数据的高度不是连续的,所以需要动态的设置偏移量。当滚动时起始索引不发生变化时,此时可以什么也不做,滚动显示的内容由浏览器控制。

以上就是不同场景下Vue中虚拟列表实现的详细内容,更多关于vue虚拟列表的资料请关注脚本之家其它相关文章!

相关文章

  • Vue中计算属性未生效:原因、排查与解决过程

    Vue中计算属性未生效:原因、排查与解决过程

    本文将深入探讨计算属性未生效的常见原因,并提供排查和解决方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • uniapp使用uview的简单案例

    uniapp使用uview的简单案例

    uView是uni-app生态专用的UI框架,uni-app 是一个使用 Vue.js 开发所有前端应用的框架,这篇文章主要给大家介绍了关于uniapp使用uview的简单案例,需要的朋友可以参考下
    2023-03-03
  • vue post application/x-www-form-urlencoded如何实现传参

    vue post application/x-www-form-urlencoded如何实现传参

    这篇文章主要介绍了vue post application/x-www-form-urlencoded如何实现传参问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-04-04
  • Vue3实现挂载全局方法和用getCurrentInstance代替this

    Vue3实现挂载全局方法和用getCurrentInstance代替this

    这篇文章主要介绍了Vue3实现挂载全局方法和用getCurrentInstance代替this,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • tdesign vue初始化组件源码解析

    tdesign vue初始化组件源码解析

    这篇文章主要为大家介绍了tdesign vue初始化组件源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • vue组件Prop传递数据的实现示例

    vue组件Prop传递数据的实现示例

    本篇文章主要介绍了vue组件Prop传递数据的实现示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • Vue + Node.js + MongoDB图片上传组件实现图片预览和删除功能详解

    Vue + Node.js + MongoDB图片上传组件实现图片预览和删除功能详解

    这篇文章主要介绍了Vue + Node.js + MongoDB图片上传组件实现图片预览和删除功能,结合实例形式详细分析了Vue + Node.js + MongoDB基于图片上传组件实现图片预览和删除功能相关操作技巧,需要的朋友可以参考下
    2020-04-04
  • Vue实现红包雨小游戏的示例代码

    Vue实现红包雨小游戏的示例代码

    红包也叫压岁钱,是过农历春节时长辈给小孩儿用红纸包裹的礼金。本文将通过Vue制作一个红包雨小游戏,文中的示例代码讲解详细,需要的可以参考一下
    2022-01-01
  • vue3使用defineModel实现父子组件双向绑定

    vue3使用defineModel实现父子组件双向绑定

    这篇文章主要个给大家介绍了在vue3中使用defineModel进行父子组件中的双向绑定,文中通过代码示例给大家介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-01-01
  • vue实现右键弹出菜单

    vue实现右键弹出菜单

    这篇文章主要为大家详细介绍了vue实现右键弹出菜单,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-07-07

最新评论