vue高性能虚拟滚动列表

当前页显示100w多条数据,不做分页的情况进行渲染。加载和渲染页面会非常慢,滚动也非常卡顿

解决方案:

1.渲染可视窗口的列表,其他列表不进行渲染。通过修改偏移量高度进行滚动列表

       2.分段插入,同时使用window.requestAnimationFrame()函数,按帧执行你插入数据的函数

vue2版本

virtualList 组件包
注意:组件必传containerHeight高度(容器高度)和数据列表listData

<template>
  <div>
    <div ref="listRef" :style="{height:containerHeight}" class="listContainer" @scroll="scrollEvent($event)">
      <div class="listPhantom" :style="{ height: computedListHeight + 'px' }"></div>
      <div class="list" ref="infiniteListRef" :style="{ transform: computedGetTransform }">
        <div ref="items"
             class="listItem"
             v-for="(item,key) in computedVisibleData"
             :key="key+'-'+item?.id"
             :style="{height:'100%' }"
        ><slot :data="item" /></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'virtualList',
  props: {
    //所有列表数据
    listData:{
      type:Array,
      default:()=>[]
    },
    //容器高度
    containerHeight:{
      type:String,
      default:"100%"
    }
  },
  watch:{
    //监听列表数据
    listData:{
      handler() {
        //修改每一个列的高度
        this.$nextTick(()=>{
          //获取每个列表高度
          this.itemHeight = this.$refs.items ? this.$refs.items[0].offsetHeight:1
          this.end = this.start + this.computedVisibleCount;
        })
      },
      deep: true,
      immediate: true
    }
  },
  computed:{
    //获取真实显示列表数据
    computedVisibleData(){
      return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
    },
    //列表总高度
    computedListHeight(){
      return BigInt(this.listData.length * this.itemHeight);
    },
    //可显示的列表项数
    computedVisibleCount(){
      return Math.ceil(this.screenHeight / this.itemHeight)
    },
    //偏移量对应的style
    computedGetTransform(){
      return `translate3d(0,${this.startOffset}px,0)`;
    }
  },
  data() {
    return {
      //每列高度
      itemHeight:1,
      //可视区域高度
      screenHeight:0,
      //偏移量
      startOffset:0,
      //起始索引
      start:0,
      //结束索引
      end:null,
    };
  },
  methods: {
    scrollEvent() {
      //当前滚动位置
      let scrollTop = this.$refs.listRef.scrollTop;
      //此时的开始索引
      this.start = Math.floor(scrollTop / this.itemHeight);
      //此时的结束索引
      this.end = this.start + this.computedVisibleCount;
      //此时的偏移量
      this.startOffset = scrollTop - (scrollTop % this.itemHeight);

    },
    //页面初始化
    handleInit(){
      this.screenHeight = this.$el.clientHeight;//客户端高度
      this.start = 0;//列表开始索引
      this.end = this.start + this.computedVisibleCount;//列表结束索引
    }
  },
  mounted() {
    this.handleInit()
  },
}
</script>

<style scoped lang="less">
.listContainer {
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
}

.listPhantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
  text-align: center;
  z-index:10;
}

.listItem {
  padding: 10px;
  color: #555;
  box-sizing: border-box;
}
</style>

app.vue引用组件的文件
分段插入数据 

<template>
  <div id="app">
    <div style="background-color: yellow">
      <VirtualList ref="testRef" :containerHeight="containerHeight" :listData="rows" >
        <template slot-scope="row">
          <div style="border-bottom: 1px solid red;">
            {{ row.data.id }}
          </div>
        </template>
      </VirtualList>
    </div>
  </div>
</template>
<script>
import VirtualList from './components/virtualList.vue';

export default {
  data(){
    return {
      once:0,//每次插入的数量
      containerHeight:0,
      total:0,//总条数
      countRender:0,//已经渲染次数
      loopCount:0,//需要插入的次数
      rows:[]
    }
  },
  components:{VirtualList},
  created() {
    this.containerHeight = 300 +'px';
  },
  mounted() {
    this.handleInit();
  },
  methods:{
    handleInit(){
      setTimeout(()=>{
        // 百万条数据
        this.total = 10000000000;
        // 单次插入 可自定义
        this.once = 20;
        // 需要插入的次数 向上取整
        this.loopCount = Math.ceil(this.total / this.once);
        // 当前渲染次数
        this.countRender = 0;
        this.handleRender();
      },500)
    },
    //百万数据分段插入
    handleRender(){
      for (let i = 0; i < this.once; i++) {
        this.rows.push({id:this.countRender+'-'+i})
      }
      // 渲染次数加1,控制递归的次数
      this.countRender++;

      if (this.countRender < this.loopCount) {
        window.requestAnimationFrame(this.handleRender);
      }
    }
  }
}
</script>
<style>
#app {
  height: 500px;
  width: 100%;
}
</style>

vue3版本

virtualList 组件包
注意:组件必传data高度(容器高度)和数据列表containerHeight

<template>
  <div>
    <div class="listContainer" :style="{height:props.containerHeight}" @scroll="handleScroll">
      <div class="listContainerPhantom" :style="{ height: listHeight + 'px' }"></div>
      <div class="listContainerList" :style="{ transform: computedTransform }">
        <div ref="listItemRef"  :key="index+'-'+item?.id" v-for="(item,index) in computedVisibleData">
          <slot :data="item"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {computed, defineProps, nextTick, onMounted, ref, watch, withDefaults} from "vue";

let startNum = ref(0)
let endNum = ref(0)

let startOffset = ref(0);//偏移量
let listHeight = ref(0);//列表总高度
let listItemHeight = ref(0);//每项高度
let listItemRef = ref(null);

interface propsInterface{
  data?:any              //列表数据
  containerHeight?:string   //容器高度
}
let props = withDefaults(defineProps<propsInterface>(),{
  //所有数据
  data:[],
  containerHeight:"",
})

const computedTransform = computed(()=>{
  return `translate3d(0,${startOffset.value}px,0)`;
})
//可看得见的数据
const computedVisibleData = computed(()=>{
  return props.data.slice(startNum.value,Math.min(endNum.value,props.data.length))
})


const handleScroll = ()=>{
  let listContainer = document.querySelector(".listContainer");//列表容器
  let listContainerHeight = listContainer?.offsetHeight

  //可显示的数量
  let showNum = Math.ceil(listContainerHeight / listItemHeight.value)

  //滚动高度
  let contentScrollTop = Math.floor(listContainer.scrollTop);

  let curNum = Math.ceil(contentScrollTop / listItemHeight.value);
  startNum.value = curNum
  endNum.value = showNum + startNum.value

  //滚动高度减去空隙
  startOffset.value = contentScrollTop - (contentScrollTop%showNum)

}

const handleInit = ()=>{
  let listContainer = document.querySelector(".listContainer");//列表容器
  let listItem = listItemRef.value ;//每一列

  let listContainerHeight = listContainer?.offsetHeight
  listItemHeight.value = listItem ? listItem[0]?.offsetHeight : 1

  //列表总高度
  listHeight.value = props.data.length*listItemHeight.value

  startNum.value = 0
  let showNum = Math.ceil(listContainerHeight / listItemHeight.value)
  endNum.value = startNum.value + showNum
}
//监听列表高度,有变化,重新初始化
watch(()=>props.data,(value)=>{
  handleInit()
},{immediate:true,deep:true})

onMounted(()=>{
  handleInit()
})
</script>

<style scoped lang="less">
.listContainer{
  height:400px;
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
  .listContainerPhantom{
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }
  .listContainerList{
    left: 0;
    right: 0;
    top: 0;
    position: absolute;
    text-align: center;
  }

}
</style>

 app.vue引用组件的文件
分段插入数据 

<template>
  <div class="app-container">
    <VirtualList :data="data" :containerHeight="virtualListHeight" >
      <template #default="row">
        <div class="listItem u-f u-f-ac u-f-spa" >
          {{row.data.id}}
        </div>
      </template>
    </VirtualList>
  </div>
</template>

<script setup lang="ts">
import {defineAsyncComponent, onMounted, ref} from "vue";
const VirtualList = defineAsyncComponent(()=>import("@/components/virtualList/index"))

let once = ref(0)//每次插入的数量
let total = ref(0)//总条数
let countRender = ref(0)//已经渲染次数
let loopCount = ref(0)//需要插入的次数
let data = ref([])
let virtualListHeight = ref("")


const handleInit = () => {
  data.value.length = 0;
  setTimeout(()=>{
    // 百万条数据
    total.value = 10000;
    // 单次插入 可自定义
    once.value = 20;
    // 需要插入的次数 向上取整
    loopCount.value = Math.ceil(total.value / once.value);
    // 当前渲染次数
    countRender.value = 0;
    handleRender();
  },500)
}
//百万数据分段插入
const handleRender = () => {
  for (let i = 0; i < once.value; i++) {
    data.value.push({id:countRender.value+'-'+i})
  }
  // 渲染次数加1,控制递归的次数
  countRender.value++;

  if (countRender.value < loopCount.value) {
    window.requestAnimationFrame(handleRender);
  }
}
onMounted(()=>{
  virtualListHeight.value = (document.documentElement.offsetHeight-400)+"px"
  handleInit()
})
</script>

<style scoped lang="less">
.app-container{
  background-color: gray;
  height:500px;
  width: 500px;
}
.listItem{
  border-bottom:1px solid red;
  padding:10px;
}
</style>

案例源码

https://gitee.com/derekgo/tool-collection/tree/dev/vue/virtualList

 本文引用于沉默小管

 【vue】vue高性能虚拟滚动列表【vue2和vue3版组件封装】_vue 虚拟滚动_沉默小管的博客-CSDN博客

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值