如何用 Vue 3 搞定千条数据的渲染?这个前端工程师用了个小技巧,颜值爆表,代码也很帅!

351 阅读1分钟

前言

当我们需要渲染大量数据时,常规的列表渲染方式会面临性能问题,因为浏览器需要一次性渲染所有的数据。而虚拟列表的原理是只渲染当前可见区域内的数据,而非全部渲染,这样可以有效提高列表的渲染性能。

原理

可以通过监听滚动事件来计算当前可见区域的数据范围,并且使用 CSS 的 transform 属性来实现滚动效果。另外,由于只渲染当前可见区域的数据,因此可以提前计算列表项的高度,进而计算出容器的高度,以便在滚动时准确地计算当前可见区域的数据范围。

总的来说,虚拟列表的原理就是在滚动时只渲染当前可见区域的数据,通过计算容器高度和滚动偏移量来确定当前可见区域的数据范围,从而提高列表的渲染性能。

上代码

<template>
  <div class="virtual-list-wrapper" :style="{ height: containerHeight + 'px' }">
    <div class="virtual-list" :style="{ transform: 'translateY(' + offsetY + 'px)' }">
      <div v-for="(item, index) in visibleData" :key="index" class="virtual-list-item">
        {{ item }}
      </div>
    </div>
  </div>
</template>

<script>
import { ref, reactive, watch, nextTick } from 'vue';

export default {
  props: {
    data: {
      type: Array,
      required: true,
    },
    itemHeight: {
      type: Number,
      default: 50,
    },
    visibleItemCount: {
      type: Number,
      default: 10,
    },
  },
  setup(props) {
    const startIndex = ref(0);
    const endIndex = ref(props.visibleItemCount - 1);
    const offsetY = ref(0);
    const containerHeight = ref(props.visibleItemCount * props.itemHeight);

    const visibleData = reactive([]);

    const updateVisibleRange = () => {
      const startIndexVal = Math.floor(offsetY.value / props.itemHeight);
      const endIndexVal = startIndexVal + props.visibleItemCount - 1;

      startIndex.value = Math.max(0, startIndexVal);
      endIndex.value = Math.min(props.data.length - 1, endIndexVal);
    };

    watch(offsetY, updateVisibleRange);

    const containerRef = ref(null);

    nextTick(() => {
      containerHeight.value = containerRef.value.clientHeight;
      updateVisibleRange();
    });

    return {
      startIndex,
      endIndex,
      offsetY,
      containerHeight,
      visibleData,
      containerRef,
      updateVisibleRange,
    };
  },
  watch: {
    data: {
      immediate: true,
      handler(newValue) {
        this.visibleData.splice(0, this.visibleData.length, ...newValue.slice(this.startIndex, this.endIndex + 1));
      },
    },
    visibleData(newValue) {
      this.$emit('update:visibleData', newValue);
    },
  },
};
</script>

<style scoped>
.virtual-list-wrapper {
  overflow: hidden;
}

.virtual-list {
  position: relative;
  transition: transform 0.3s ease;
}

.virtual-list-item {
  height: 50px;
}
</style>