Vue3 实现虚拟长列表

1,602 阅读1分钟

html

<body>
   <div id="app"></div>
   <script src="https://unpkg.com/vue@next"></script>
</body>

css

<style>
  * {
    box-sizing: border-box;
    margin: 0;
  }
  .container {
    height: 300px;
    overflow-y: scroll;
  }
  .item {
    border: 1px solid #eee;
    line-height: 30px;
    height: 30px;
    padding: 0 10px;
    cursor: pointer;
  }
</style>

js

<script>
  let t = Date.now()
  const  { ref, reactive, computed, onMounted, createApp } = Vue;
  createApp({
    template: `  
      <div class="container" @scroll="onScroll">
        <div class="panel" ref="panel"
             :style="{paddingTop: paddingTop + 'px'}">
          <div class="item" v-for="item in visibleList" :key="item">
            {{ item }}
          </div>
        </div>
      </div>`,
    
    setup() {
      let panel = ref(null)  //列表容器DOM
      
      //构造的长列表原始数据
      let raw = Array(100000).fill(0).map((v, i) => `item-${i}`); 
      let count = 10;      //实际渲染DOM的列表数量
      let start = ref(0);  //从长列表数组总截取数据的起点 
      let end = ref(10);   //从长列表数组总截取数据的终点
      let itemHeight = 30; //单个列表项的高度
      let paddingTop = ref(0); //列表容器的上内边距
      let totalHeight = raw.length*itemHeight  //原始数据理论上完全渲染后占据的总高度

      let visibleList = computed(() => raw.slice(start.value, end.value)); //根据起点和终点获取要渲染的数据
      onMounted(() => panel.value.style.height = totalHeight + 'px') //在mounted后设置列表容器的高度
      
      //滚动-->根据滚动距离计算起点和终点的下标-->计算属性得到visibleList-->真实DOM被替换 同时设置paddingTop让元素视觉上没跳动
      const onScroll = function (e) {
        start.value = Math.floor(e.target.scrollTop / itemHeight); //当滚动后,重新计算起点的位置
        end.value = start.value + count; //设置终点的位置
        paddingTop.value = start.value*itemHeight; 
      };

      return {
        visibleList, paddingTop, panel, onScroll
      };
    }
  }).mount('#app');
</script>

预览链接

vue3虚拟长列表

优化

  • 可视区域高度不做限制,列表子项高度不固定,展示列表的数量可根据可视区域高度自动计算
  • 对滚动做使用防抖或者节流处理,减少计算频率
  • 第2条可提升性能,但会延长滚动时上下方出现空白的时间。可通过加列表Buff来处理(如可视区域展示10个列表,再其上方隐藏10个列表,其下方隐藏10个列表)
<template>
<div class="container" @scroll="onScroll" ref="container">
<div class="panel" ref="panel" :style="{ paddingTop: paddingTop + 'px' }">
<div class="item" v-for="item in visibleList" :key="item">{{ item }}</div>
</div>
</div>
</template>

<script>
import { ref, computed, onMounted } from "vue";
export default {
setup() {
let container = ref(null),
panel = ref(null);
let buffTop = 100,
buffBottom = 100,
count = 0;
let raw = Array(100000).fill(0).map((v, i) => `item-${i}`);
let start = ref(0),
end = ref(1);
let itemHeight = 1;
let paddingTop = ref(0);
let visibleList = computed(() => raw.slice(start.value, end.value));
onMounted(() => {
itemHeight = panel.value.firstElementChild.offsetHeight;
panel.value.style.height = raw.length * itemHeight + "px";
count = Math.floor(container.value.offsetHeight / itemHeight);
end.value = count + buffBottom;
});
let timer = null;
const onScroll = function (e) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
let startValue = Math.floor(e.target.scrollTop / itemHeight);
let buff = startValue > buffTop ? buffTop : startValue;
start.value = startValue - buff;
end.value = startValue + count + buffBottom;
paddingTop.value = start.value * itemHeight;
}, 200);
};
return {
visibleList,
paddingTop,
container,
panel,
onScroll,
    };
  },
};
</script>

<style>
* {
box-sizing: border-box;
margin: 0;
}
.container {
height: 100vh;
overflow: scroll;
}
.panel {
border: 1px solid red;
}
.item {
border: 1px solid #eee;
padding: 6px 10px;
cursor: pointer;
}
</style>

组件封装

<template>
  <LongList :data=data :startBuff='startBuff' :endBuff='endBuff'/>
</template>