项目没亮点,那就编!!!

115 阅读3分钟

虚拟列表

问题

服务端,一次性给前端 50000 条数据?????

现象

  • 简单模拟一下
<script setup lang="js">
import { nextTick, reactive } from 'vue';

const data = reactive([])
const loadData = () => {
  data.length = 0 // 这里可以看到 key 的作用
  console.time('start')
  console.time('render')
  for(let i = 0; i < 50000; i++){
    data.push(i)
  }
  console.timeEnd('start')
  nextTick(() => {
    console.timeEnd('render')
  })
  // debugger
}
</script>

<template>
  <button class="butten" @click="loadData">加载数据</button>
  <div class="list-item" v-for="item in data" :key="item">{{ item }}</div>
</template>
<style scoped>
.list-item {
  background: gray;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 8px;
}
</style>
  • 可以看到数据 push 用时 89ms,但是渲染数据要 1.07s,不管什么时候, 你要相信js 永远比dom 快的多。
  • 会发现页面空白时间较长,这是不可以接受的

解决方法

1. 分片加载

  • 每 500 个渲染
  • 修改代码
  <script setup lang="js">
  import { nextTick, reactive } from 'vue';
  
  let data = reactive([])
  const loadData = async () => {
    data.length = 0 // 这里可以看到 key 的作用
    let cachData = []
    let num = 0
    let disance = 500
    console.time('start')
    console.time('render')
    for (let i = 0; i < 50000; i++) {
      cachData.push(i)
      if (cachData.length % disance === 0) {
        await new Promise((resolve) => {
          setTimeout(() => {
            data.push(...cachData.slice(num * disance, num * disance + disance + 1))
            num++
            resolve()
          })
        })
  
      }
    }
    console.log('last-data::', data)
    console.timeEnd('start')
    nextTick(() => {
      console.timeEnd('render')
    })
    // debugger
  }
  </script>
  
  <template>
    <button class="butten" @click="loadData">加载数据</button>
    <div class="list-item" v-for="item in data" :key="item">{{ item }}</div>
  </template>
  <style scoped>
  .list-item {
    background: gray;
    border-radius: 8px;
    padding: 12px;
    margin-bottom: 8px;
  }
  </style>
  • 看效果
  • 解决了一开始页面空白的问题
  • 但是定时器较多,用户进来不可能一下子全部看完
  • await 使用过多会导致性能问题
  • 使用 Promise.all进行优化
<script setup lang="js">
import { nextTick, reactive } from 'vue';

let data = reactive([])
const loadData = async () => {
  data.length = 0 // 这里可以看到 key 的作用
  let cacheData = []
  let num = 0
  let distance = 500
  console.time('start')
  console.time('render')
  const promises = [];

  for (let i = 0; i < 50000; i++) {
    cacheData.push(i);

    if (cacheData.length % distance === 0) {
      const promise = new Promise((resolve) => {
        setTimeout(() => {
          data.push(...cacheData.slice(num * distance, num * distance + distance + 1));
          num++;
          resolve();
        });
      });

      promises.push(promise);
    }
  }

  // 使用 Promise.all 来等待所有异步任务完成
  await Promise.all(promises);
  console.log('last-data::', data)
  console.timeEnd('start')
  nextTick(() => {
    console.timeEnd('render')
  })
  // debugger
}
</script>

<template>
  <button class="butten" @click="loadData">加载数据</button>
  <div class="list-item" v-for="item in data" :key="item">{{ item }}</div>
</template>
<style scoped>
.list-item {
  background: gray;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 8px;
}
</style>

2. 虚拟列表

Vue 虚拟列表原理解析

在前端开发中,处理大型数据集合的列表渲染是一项常见的挑战。虚拟列表是一种优化手段,通过只渲染当前视窗可见部分的数据,降低了性能开销。本文将深入探讨虚拟列表的原理,特别是在 Vue 中的实现方式。

基本原理

虚拟列表的基本原理是根据用户滚动的位置,动态计算当前可见区域的数据,并只渲染这一部分数据。具体步骤如下:

计算可见区域

根据滚动条的偏移量,计算出当前可见区域应该从哪一个数据项开始渲染,这个位置也称为起始索引。

startIndex = Math.floor(scrollTop / itemHeight);

计算结束索引

根据起始索引、渲染数量和缓冲数量等信息,计算出当前可见区域的结束索引。

endIndex = Math.min(startIndex + renderCount + bufferCount, data.length);

提取可见数据

将起始索引到结束索引之间的数据项提取出来,更新列表的渲染数据。

visibleData = data.slice(startIndex, endIndex);

设置偏移

通过设置容器元素的 transform 属性,实现将列表项向上或向下偏移的效果,确保只显示当前可见的数据。

listHeight.style.transform = `translateY(${startIndex * itemHeight}px)`;

如果不加 listHeight.value.style.transform 这一行,可能会导致虚拟滚动列表在滚动时不会正确更新显示的数据。这行代码的作用是根据当前滚动位置计算应该渲染哪一部分的数据,并通过设置 transform 属性实现虚拟滚动的效果。

具体而言,这行代码实现了以下几个功能:

  1. 计算起始索引: 根据滚动条的偏移量,计算出当前可见区域应该从哪一个数据项开始渲染。
  2. 计算结束索引: 根据起始索引、渲染数量和缓冲数量等信息,计算出当前可见区域的结束索引。
  3. 更新可见数据: 将起始索引到结束索引之间的数据项提取出来,更新 visibleData 的值,从而确保只有当前可见的数据项会被渲染。
  4. 设置偏移: 通过设置 listHeight 元素的 transform 属性,实现将列表项向上或向下偏移的效果,从而显示正确的数据区域。

如果不执行这些操作,可能会导致滚动时列表显示混乱,用户无法正确看到预期的数据。

看效果

完整代码

<template>
  <div class="list-box" ref="listBox" @scroll="scrollHandler">
    <div
      class="list-height"
      ref="listHeight"
      :style="{ height: `${totalHeight}px` }"
    >
      <div
        class="list-item"
        v-for="item in visibleData"
        :key="item.id"
        :style="{
          height: `${itemHeight}px`,
          transform: `translateY(${item.offsetTop}px)`,
        }"
      >
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup lang="js">
import { ref, onMounted } from "vue";

const totalHeight = ref(0);
const listHeight = ref('');
const listBox = ref('')
const itemHeight = 40; // 每个项的高度
const renderCount = 10; // 渲染的项数
const bufferCount = 5; // 缓冲的项数
const startIndex = ref(0); // 起始索引
const endIndex = ref(0); // 结束索引
const visibleData = ref([]); // 当前可见的数据
const data = ref([]); // 模拟的列表数据

const initData = () => {
  // 模拟数据
  for (let i = 0; i < 50000; i++) {
    data.value.push({ id: i, value: `Item ${i + 1}` });
  }
};

const calculateTotalHeight = () => {
  // 计算容器总高度
  totalHeight.value = data.value.length * itemHeight;
};

const updateVisibleData = () => {
  // 滚懂条的偏移量
  const scrollTop = listBox.value.scrollTop;
  // 滚动的位置距离顶部的距离能容纳多少 item
  startIndex.value = Math.floor(scrollTop / itemHeight);
  endIndex.value = Math.min(
    Math.ceil(scrollTop / itemHeight + renderCount + bufferCount),
    data.value.length
  );
  // 更新可视区域的数据
  visibleData.value = data.value.slice(startIndex.value, endIndex.value);
  listHeight.value.style.transform = `translateY(${
    startIndex.value * itemHeight
  }px)`;
};

const scrollHandler = () => {
  updateVisibleData();
};

onMounted(() => {
  initData();
  calculateTotalHeight();
  updateVisibleData();
});
</script>

<style scoped>
.list-item {
  background: #f0f0f0;
  border-bottom: 1px solid #ddd;
  line-height: 40px;
  text-align: center;
}

.list-box {
  height: 300px;
  overflow-y: auto;
}

.list-height {
  position: relative;
  will-change: transform;
}
</style>