虚拟列表的实现

一、什么是虚拟列表

         在传统的列表渲染中,如果列表数据过多,一次性渲染所有数据将耗费大量的时间和内存。当我们上下滚动时,性能低的浏览器或电脑都会感觉到非常的卡,这对用户的体验时是致命的。

        于是我们会想到懒加载,当资源到达可视窗口内时,继续向服务器发送请求获取接下来的资源,不过当获取的资源越来越多时,此时浏览器不断重绘与重排,这样的开销也是要考虑的当数量多到一定程度时,页面也会出现卡顿。 

         此时我们会想到虚拟列表,虚拟列表只渲染当前可见的部分数据,随着滚动条的滚动,只渲染当前可见的列表项,从而大大减少了渲染时间。同时支持无限滚动,用户只需要不停地滚动页面,就可以看到所有的数据,从而提高了用户的体验。

        另外,需要注意的是,懒加载和虚拟列表也是有区别的:

  1. 实现方式不同。懒加载是将页面上的图片、视频等资源延迟加载,只有当用户将它们滚动到可视区域时才会加载,从而减少页面的加载时间。虚拟列表是将大型列表数据分段加载,只渲染当前可见的部分数据,随着滚动条的滚动,只渲染当前可见的列表项,从而大大减少了渲染时间。
  2. 优化的点不同。懒加载主要是针对页面上的资源进行优化,减少页面的加载时间,这对网络繁忙卡顿有帮助。虚拟列表主要是针对大型列表数据进行优化,减少渲染时间和内存占用。
  3. 使用场景不同。懒加载适用于需要加载大量图片、视频等资源的页面,如图片展示、视频播放等。虚拟列表适用于需要渲染大量数据的页面,如电商网站中的商品列表、社交网站中的好友列表等

二、虚拟列表的实现

        

         盗用一张很经典的图,我们用数组保存要渲染的数据,同时监听鼠标滚动事件,获取滚动的距离,通过相关计算我们计算出滚动距离下对应的数组对应的渲染的开始索引和结束索引,用slice方法截取出来得到新数组,把新数组渲染到页面。以下是具体实现

三、代码讲解

        (1)html和css

                强调一下第三个div的作用(.list-view-shadow),高度设置为装下所有列表的总高度,触发滚动条的出现;ref="content"这个div就是渲染区域;其他的不多于讲解

<template>
  <div class="father">
    <div class="list-view" ref="scrollBox" @scroll="handleScroll">
      <div
        class="list-view-shadow"
        :style="{
          height: contentHeight,
        }"
      ></div>
      <div ref="content" class="list-view-content">
        <div
          class="list-view-item"
          :style="{
            height: item.height + 'px',
          }"
          v-for="item in state.visibleData"
          :key="item.val"
        >
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</template>

 <style scoped>
.father {
  width: 500px;
}
.list-view {
  height: 400px;
  overflow: auto;
  position: relative;
  border: 1px solid #aaa;
}

.list-view-shadow {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.list-view-content {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}
.list-view-content div:nth-child(2n + 1) {
  background-color: rgb(222, 85, 108);
}
.list-view-item {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #666;
  box-sizing: border-box;
  background-color: rgb(61, 217, 234);
  border: 1px solid snow;
}

</style>

        (2)核心代码

                1)数据初始化,data用来存储传进来的数据,这里我们自己使用随机函数生成高度数据,数组computedData是辅助数组,里面存放到i元素(包括自身)的累加高度,后面滚动后参考比较滚动距离和累计高度(后面有),从而算出索引,再计算content(dom元素)该translate的距离

let scrollBox = ref(null).value;//获取dom元素
let content = ref(null).value;
let contentHeight = computed({
  get() {
    return computedData[computedData.length - 1].bom + "px";
  },
});

onMounted(() => {
  updateVisibleData();//首次渲染数据
});

let data = [];//存储随机高度,想要固定高度列表的童鞋把随机函数去掉即可
let minHeight = 35;//设置最小高度
for (let i = 0; i < 100; i++) {
  //这里val相当于列表id啦
  data.push({ val: i, height: minHeight + Math.random() * 20 });
}

let computedData = [];//辅助数组存储累加高度,用来修正高度,计算translate距离
for (let i = 0; i < data.length; i++) {
  let bom = data[i].height + (i === 0 ? 0 : computedData[i - 1].bom);
  computedData[i] = { val: data[i].val, bom };
}


//visibleData用来存储展示的数据
//start用来记录该渲染的数据的首个索引
let state = reactive({
  visibleData: [],
  start:0
});

                2)实现更新渲染数组的函数(重点)。首先根据木桶效应计算出渲染列表容量,各位想想如果不用minHeight会出现什么效果(大家可以shishi);这个state.start是个响应式数据,后面滚动发生高度变化,二分法计算出首个索引,state.start变化时会触发这个更新函数(下面有);然后从原数组中截取需要渲染的数组,页面绑定了这个数组,所以视图会更新;

                      接下来计算滚动距离,累计到当前索引高度 - 当前索引所占高度,这就实现了ref为content这个div不偏移过头,确保里面列表计算出来的首个索引的元素可以进入视图

        

function updateVisibleData() {
  //计算最大容量(渲染呈现的区域 / 最小高度)
  const visibleCount = Math.ceil(scrollBox.clientHeight / minHeight);
  const end = state.start + visibleCount;
  state.visibleData = data.slice(state.start, end);
  //计算滚动距离(累计到当前索引高度 - 当前索引所占高度,这就实现了视图包含当前索引的那项div)
  let scrollLen = computedData[state.start].bom - data[state.start].height;
  //设置偏移距离(因为鼠标一直在滚动,不设置的话这个呈现列表就在原地不动啦)
  content.style.webkitTransform = `translate3d(0, ${scrollLen}px, 0)`;
}

                3)监听鼠标滚动事件,触发findStart函数,在findStart中,使用二分法寻找computedData[i].bom最先大于scrollTop的索引下标,为什么要是判断其底部是否大于scrollTop呢?因为如果滚动距离没有超过一个小列表的底部(累加)距离时(下图简称bom),我们这个列表应该还是可见的,不然页面不连贯 ,如下图

       

//用watch来监听start是否发生变化,如果发生变化才调用处理函数,这个可以提升性能
watch(()=>state.start,(newval,oldval)=>{
  if(newval!==oldval){
    updateVisibleData();
  }
})

function handleScroll() {
  const scrollTop = scrollBox.scrollTop;//获取滚动距离
  state.start = findStart(scrollTop);
  
}

function findStart(scrollTop) {
  let left = 0;
  let right = computedData.length;
  //二分法寻找start
  while (left + 1 != right) {
    let mid = Math.floor((left + right) / 2);
    if (computedData[mid].bom > scrollTop) {
      right = mid;
    } else {
      left = mid;
    }
  }
  //为什么要返回right,因为我们1判断的是bom属性,按照上面的算法来说,right下标下面的bom属性永远大于滚动距离----
  //left下标对于的bom永远小于等于滚动距离,也就是说left索引对应的元素可以不用渲染;如果滚动距离没超过某个索引下的bom属性时,当前索引有效(也就是right)。
  return left === 0 ? 0 : right;
}

         另外为了减少触发updateVisibleData函数的触发,需要设置watch监听器,当state.start也就是渲染数据的索引改变时,我们才调用updateVisibleData函数。

四、全部代码

<template>
  <div class="father">
    <div class="list-view" ref="scrollBox" @scroll="handleScroll">
      <div
        class="list-view-shadow"
        :style="{
          height: contentHeight,
        }"
      ></div>
      <div ref="content" class="list-view-content">
        <div
          class="list-view-item"
          :style="{
            height: item.height + 'px',
          }"
          v-for="item in state.visibleData"
          :key="item.val"
        >
          {{ item.val }}--height--{{ item.height }}
        </div>
      </div>
    </div>
  </div>
</template>
    
<script setup>
import { onMounted, computed, ref, reactive, watch } from "vue";
let scrollBox = ref(null).value; //获取dom元素
let content = ref(null).value;
let contentHeight = computed({
  get() {
    return computedData[computedData.length - 1].bom + "px";
  },
});

onMounted(() => {
  updateVisibleData(); //首次渲染数据
});

let data = []; //存储随机高度,想要固定高度列表的童鞋把随机函数去掉即可
let minHeight = 35; //设置最小高度
for (let i = 0; i < 100; i++) {
  //这里val相当于列表id啦
  data.push({ val: i, height: minHeight + Math.random() * 20 });
}

let computedData = []; //辅助数组存储累加高度,用来修正高度
for (let i = 0; i < data.length; i++) {
  let bom = data[i].height + (i === 0 ? 0 : computedData[i - 1].bom);
  computedData[i] = { val: data[i].val, bom };
}

//visibleData用来存储展示的数据
//start用来记录该渲染的数据的首个索引
let state = reactive({
  visibleData: [],
  start: 0,
});

//用watch来监听start是否发生变化,如果发生变化才调用处理函数,这个可以提升性能
watch(
  () => state.start,
  (newval, oldval) => {
    if (newval !== oldval) {
      updateVisibleData();
    }
  }
);

function updateVisibleData() {
  //计算最大容量(渲染呈现的区域 / 最小高度)
  const visibleCount = Math.ceil(scrollBox.clientHeight / minHeight);
  const end = state.start + visibleCount;
  state.visibleData = data.slice(state.start, end);
  //计算滚动距离(累计到当前索引高度 - 当前索引所占高度,这就实现了视图包含当前索引的那项div)
  let scrollLen = computedData[state.start].bom - data[state.start].height;
  //设置偏移距离(因为鼠标一直在滚动,不设置的话这个呈现列表就在原地不动啦)
  content.style.webkitTransform = `translate3d(0, ${scrollLen}px, 0)`;
}

function handleScroll() {
  const scrollTop = scrollBox.scrollTop; //获取滚动距离
  state.start = findStart(scrollTop);
}

function findStart(scrollTop) {
  let left = 0;
  let right = computedData.length;
  //二分法寻找start
  while (left + 1 != right && right > 0) {
    let mid = Math.floor((left + right) / 2);
    if (computedData[mid].bom > scrollTop) {
      right = mid;
    } else {
      left = mid;
    }
  }
    //为什么要返回right,因为我们1判断的是bom属性,按照上面的算法来说,right下标下面的bom属性永远大于滚动距离----
  //left下标对于的bom永远小于等于滚动距离,也就是说left索引对应的元素可以不用渲染;如果滚动距离没超过某个索引下的bom属性时,当前索引有效(也就是right)。
  return left === 0 ? 0 : right;
}
</script>
    <style scoped>
.father {
  width: 500px;
}
.list-view {
  height: 400px;
  overflow: auto;
  position: relative;
  border: 1px solid #aaa;
}

.list-view-shadow {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
  visibility: hidden;
}

.list-view-content {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}
.list-view-content div:nth-child(2n + 1) {
  background-color: rgb(222, 85, 108);
}
.list-view-item {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #666;
  box-sizing: border-box;
  background-color: rgb(61, 217, 234);
  border: 1px solid snow;
}
</style>

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: el-table 虚拟列表是指在表格中只渲染视窗内的行和列,可以帮助您提升表格的渲染性能。 使用 el-table 虚拟列表的方法是,首先在 el-table 上设置属性 "highlight-current-row" 和 "stripe",然后在 el-table-column 中设置 "resizable" 属性以启用调整列宽的功能,最后在 el-table 上设置 "row-key" 属性以启用虚拟列表。 示例代码如下: ```html <template> <el-table :data="tableData" highlight-current-row stripe row-key="id" > <el-table-column prop="name" label="姓名" width="180" resizable ></el-table-column> <el-table-column prop="age" label="年龄" width="180" resizable ></el-table-column> </el-table> </template> <script> export default { data() { return { tableData: [ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 20 }, // ... ] } } } </script> ``` 在这个示例中,我们在 el-table 上设置了 "highlight-current-row" 和 "stripe" 属性,启用了高亮当前行和斑马纹,在 el-table-column 中设置了 "resizable" 属性,启用了调整列宽的功能,最后在 el-table 上设置了 "row-key" 属性,并指定了数据中的 "id" 字段为行的唯一标识符,从而启用了虚拟列表。 希望这些信息能帮到您! ### 回答2: el-table是饿了么团队基于Vue.js开发的一个好用的表格组件。虚拟列表是一种针对海量数据渲染的优化技术,可以大幅提升大数据表格的渲染性能和用户体验。下面是一个el-table虚拟列表实现代码示例: ``` <template> <el-table :data="visibleData" :row-key="getRowKey" :height="tableHeight" header-row-class-name="table-header" row-class-name="table-row" @row-click="handleRowClick" > <!-- 此处写入表格列的定义 --> </el-table> </template> <script> export default { data() { return { tableHeight: null, // 表格高度 totalHeight: 0, // 总数据高度 visibleData: [], // 当前可视的数据 visibleCount: null, // 可视区域的数据数量 rowHeight: 50, // 数据行高度 startIndex: 0, // 可视区域第一条数据的索引 }; }, mounted() { this.initTableHeight(); // 监听可视区域变化,当可视区域变化时重新计算startIndex window.addEventListener('scroll', this.handleScroll); }, destroyed() { window.removeEventListener('scroll', this.handleScroll); }, methods: { initTableHeight() { this.tableHeight = this.$refs.tableWrapper.clientHeight; }, handleScroll() { const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; this.startIndex = Math.floor(scrollTop / this.rowHeight); // 计算可视区域第一条数据的索引 this.updateVisibleData(); }, updateVisibleData() { this.visibleData = this.data.slice(this.startIndex, this.startIndex + this.visibleCount); }, getRowKey(row) { return row.id; // 这里的getRowKey方法用来指定每一行的唯一标识,确保列表渲染时的性能 }, handleRowClick(row) { // 处理行点击事件 }, // 其他一些方法和计算属性... }, computed: { data() { // 这里可以通过Ajax请求或其他方式获取总数据,此处仅作示例 return Array.from({ length: 10000 }, (v, i) => ({ id: i + 1, name: '用户' + (i + 1) })); }, // 计算可视区域的数据数量 visibleCount() { return Math.ceil(this.tableHeight / this.rowHeight); }, }, }; </script> ``` 以上示例代码是el-table虚拟列表的一个简单实现,通过监听滚动事件来动态计算可视区域的数据,并更新可视数据,从而实现大数据表格的优化渲染。需要注意的是,虚拟列表只渲染可视区域的数据,因此在使用上要注意一些功能的兼容性和可用性。 ### 回答3: el-table是Element UI库中的一个表格组件,用于展示和编辑数据。虚拟列表是一种优化技术,用于处理大数据量的表格,通过只渲染可见区域的数据来提高性能和加载速度。 要实现el-table的虚拟列表,可以按照以下步骤进行操作: 1. 首先,需要安装Element UI库和Vue.js框架,并将它们导入到项目中。可以使用npm或者CDN方式进行安装和引入。 2. 在Vue组件中引入el-table组件,并设置列的字段、标题等信息。 3. 在data属性中,声明一个数组变量用于存储要展示的数据。 4. 在mounted生命周期钩子中,通过Ajax请求获取所有数据,并保存在data属性中。 5. 在el-table组件上添加属性::height="300" 和 :row-key="index",其中高度可根据实际需求进行调整,row-key属性用于指定每行的唯一标识。 6. 添加一个计算属性,用于只展示可见区域的数据。在计算属性中,根据el-table当前的高度和滚动区域的位置,计算需要渲染的数据的起始索引和结束索引。 7. 在el-table组件中,使用v-for指令渲染计算属性中得到的数据。 8. 为了保证滚动时数据的平滑加载,可以使用debounce函数对window的scroll事件进行节流处理。在该事件监听函数中,修改计算属性中的索引,在滚动时实现虚拟列表的渲染。 通过以上步骤,我们可以实现el-table的虚拟列表。当用户滚动表格时,只会渲染可见区域的数据,从而提高了性能和加载速度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值