vue简单实现一个虚拟列表的示例代码

目录

一、前言

当今的时代是大数据时代,往往一个列表就有成千上万条数据,而我们一一渲染的话,则需要耗费大量时间,导致网页打开缓慢。懒加载虽然减少了第一次渲染时间,加快了网页打开速度,但随着后续数据的不断载入拼接,列表的渲染时间也会越来越长。虚拟列表则很好的解决了这一问题。

虚拟列表只渲染当前可视区域的列表,并不会将所有的数据渲染。下面用Vue简单实现移动端虚拟列表(并且支持下拉触底加载效果)

二、代码实现

准备下拉数据:

export default {
  data() {
    return {
      listData: [], // 总数据
      isLoading: false, // 展示loading
    };
  },
  mounted() {
    this.getListData();
  },
  methods: {
    // 获取数据
    getListData() {
      const count = 20 + this.listData.length;
      const start = this.listData.length;
      this.isLoading = true;
      setTimeout(() => {
        for (let i = start; i < count; i++) {
          this.listData.push(i);
        }
        this.isLoading = false;
      }, 500);
    },
  },
};

需要准备内外两个列表容器,外部容器(viewport)固定高度用于生成滚动条,内部容器(scrollbar)用于撑开外部容器使得滚动条保持与未使用虚拟列表时一致。

<template>
  <div class="viewport" ref="viewport">
    <!-- 滚动条 -->
    <div class="scrollbar" :style="{ height: listHeight + 'px' }"></div>
    <!-- 展示的列表 -->
    <div
      class="list"
      :style="{ transform: `translateY(${transformOffset}px)` }"
    >
      <div
        class="row"
        :style="{ height: rowHeight + 'px' }"
        v-for="(item, index) in showList"
        :key="index"
      >
        <slot :record="item"></slot>
      </div>
    </div>
    <!-- 加载 -->
    <div class="loading_wrap" v-show="loading">
      <div class="loading">
        <div class="container"></div>
      </div>
      <div>正在加载中</div>
    </div>
  </div>
</template>

<style lang="less" scoped>
/*
------最外层容器---------*/
.viewport {
  width: 100%;
  height: 100%; // 这个的高度让父组件去决定
  background-color: #fff;
  position: relative;
  overflow-y: auto;
}
/*
------列表展示层容器---------*/
.list {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}
/*
------每行容器---------*/
.row {
  overflow: hidden;
}

</style>

计算列表的总高度listHeight(列表的总条数乘以每一条的高度),可视区域的高度viewHeight。

计算当前可见区域起始数据的startIndex和结束数据的endIndex,监听viewport列表滚动事件,计算currentIndex以及列表的偏移量transformOffset。

监听滚动事件动态设置显示的列表(showList)。

<script lang="ts" setup>
import {
  defineProps,
  withDefaults,
  defineEmits,
  ref,
  onMounted,
  computed,
} from "vue";
interface Props {
  list: string[]; // 数据源
  rowHeight: number; // 每行的高度
  viewCount: number; // 显示数量
  loading: boolean; // 控制loading
}

const props = withDefaults(defineProps<Props>(), {
  list: () => [],
  rowHeight: 200,
  viewCount: 10,
  loading: false,
});

const emit = defineEmits<{
  (e: "bottomLoad"): void;
}>();
let viewHeight = ref(0); //可视区域的高度
let startIndex = ref(0); //开始索引
let endIndex = ref(0); //结束索引
let transformOffset = ref(0); //列表的偏移量
const viewport = ref(null);

onMounted(() => {
  initData();
});
let showList = computed(() =>
  props.list.slice(startIndex.value, endIndex.value)
); //展示的数据
let listHeight = computed(() => props.list.length * props.rowHeight); //列表的总高度

// 初始化一些数据
const initData = () => {
  endIndex.value = props.viewCount;
  viewHeight.value = viewport.value.offsetHeight;
};

// 列表滚动
const onScroll = () => {
  const scrollTop = viewport.value.scrollTop; // 获取试图往上滚动的高度
  const currentIndex = Math.floor(scrollTop / props.rowHeight); // 计算当前的索引
  // 只在需要变化的时 才重新赋值
  if (startIndex.value !== currentIndex) {
    startIndex.value = currentIndex;
    endIndex.value = startIndex.value + props.viewCount; // 结束索引
    transformOffset.value = scrollTop - (scrollTop % props.rowHeight);
  }
  // 触底了
  if (Math.round(viewHeight.value + scrollTop) === listHeight.value) {
    // 发送触底加载事件
    emit("bottomLoad");
  }
};
</script>

三、完整代码

demo.vue

<template>
  <div class="page">
    <h3>长列表渲染</h3>
    <ListScroll
      class="list_scroll"
      :list="listData"
      :loading="isLoading"
      @bottomLoad="onBottomLoad"
    >
      <template v-slot="{ record }">
        <div class="row_content" @click="handleClick(record)">
          <div>{{ record }}</div>
          <img
            class="image"
            src="/uploads/allimg/20240503/-240503115912C1.jpg"
          />
        </div>
      </template>
    </ListScroll>
  </div>
</template>

<script>
export default {
  data() {
    return {
      listData: [], // 总数据
      isLoading: false, // 展示loading
    };
  },
  mounted() {
    this.getListData();
  },
  methods: {
    // 获取数据
    getListData() {
      const count = 20 + this.listData.length;
      const start = this.listData.length;
      this.isLoading = true;
      setTimeout(() => {
        for (let i = start; i < count; i++) {
          this.listData.push(i);
        }
        this.isLoading = false;
      }, 500);
    },
    // 监听触底事件
    onBottomLoad() {
      console.log("触底了");
      if (this.listData.length >= 100) {
        console.log("数据加载完了~");
        return;
      }
      // 加载数据
      this.getListData();
    },
    // 监听点击每行
    handleClick(record) {
      console.log(record, "record");
    },
  },
};
</script>

<style lang="less" scoped>
.page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  .list_scroll {
    flex: 1;
  }
}
.row_content {
  width: 100%;
  height: 100%;
  .image {
    display: block;
    width: 100%;
    height: 160px;
    object-fit: cover;
  }
}
</style>

ListScroll.vue

<template>
  <div class="viewport" ref="viewport" @scroll="onScroll">
    <!-- 滚动条 -->
    <div class="scrollbar" :style="{ height: listHeight + 'px' }"></div>
    <!-- 展示的列表 -->
    <div
      class="list"
      :style="{ transform: `translateY(${transformOffset}px)` }"
    >
      <div
        class="row"
        :style="{ height: rowHeight + 'px' }"
        v-for="(item, index) in showList"
        :key="index"
      >
        <slot :record="item"></slot>
      </div>
    </div>
    <!-- 加载 -->
    <div class="loading_wrap" v-show="loading">
      <div class="loading">
        <div class="container"></div>
      </div>
      <div>正在加载中</div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import {
  defineProps,
  withDefaults,
  defineEmits,
  ref,
  onMounted,
  computed,
} from "vue";
interface Props {
  list: string[]; // 数据源
  rowHeight: number; // 每行的高度
  viewCount: number; // 显示数量
  loading: boolean; // 控制loading
}

const props = withDefaults(defineProps<Props>(), {
  list: () => [],
  rowHeight: 200,
  viewCount: 10,
  loading: false,
});

const emit = defineEmits<{
  (e: "bottomLoad"): void;
}>();
let viewHeight = ref(0); //可视区域的高度
let startIndex = ref(0); //开始索引
let endIndex = ref(0); //结束索引
let transformOffset = ref(0); //列表的偏移量
const viewport = ref(null);

onMounted(() => {
  initData();
});
let showList = computed(() =>
  props.list.slice(startIndex.value, endIndex.value)
); //展示的数据
let listHeight = computed(() => props.list.length * props.rowHeight); //列表的总高度

// 初始化一些数据
const initData = () => {
  endIndex.value = props.viewCount;
  viewHeight.value = viewport.value.offsetHeight;
};

// 列表滚动
const onScroll = () => {
  const scrollTop = viewport.value.scrollTop; // 获取试图往上滚动的高度
  const currentIndex = Math.floor(scrollTop / props.rowHeight); // 计算当前的索引
  // 只在需要变化的时 才重新赋值
  if (startIndex.value !== currentIndex) {
    startIndex.value = currentIndex;
    endIndex.value = startIndex.value + props.viewCount; // 结束索引
    transformOffset.value = scrollTop - (scrollTop % props.rowHeight);
  }
  // 触底了
  if (Math.round(viewHeight.value + scrollTop) === listHeight.value) {
    // 发送触底加载事件
    emit("bottomLoad");
  }
};
</script>

<style lang="less" scoped>
/*
------最外层容器---------*/
.viewport {
  width: 100%;
  height: 100%; // 这个的高度让父组件去决定
  background-color: #fff;
  position: relative;
  overflow-y: auto;
}
/*
------列表展示层容器---------*/
.list {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}
/*
------每行容器---------*/
.row {
  overflow: hidden;
}
/*
------loading样式---------*/
.loading_wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  color: #999;
  padding: 20px 0;
  .loading {
    box-sizing: border-box;
    width: 20px;
    height: 20px;
    border: 2px solid #ddd;
    border-radius: 50%;
    animation: rotate 1s linear infinite;
    margin-right: 10px;
  }
  .container {
    position: relative;
    top: 50%;
    left: 50%;
    width: 10px;
    height: 10px;
    background-color: #fff;
  }
}
/*
------loading动画---------*/
@keyframes rotate {
  from {
    transform-origin: center center;
    transform: rotate(0deg);
  }
  to {
    transform-origin: center center;
    transform: rotate(360deg);
  }
}
</style>

四、实现效果

五、实现效果

实现虚拟列表就是处理滚动条滚动后的可见区域的变更,具体实现步骤如下:

  • 计算当前可见区域起始数据的startIndex
  • 计算当前可见区域借宿数据的endIndex
  • 计算当前可见区域的数据,并渲染到页面中
  • 计算startIndex对应的数据在整个列表中的偏移位置transformOffset,并设置到列表上

到此这篇关于vue简单实现一个虚拟列表的示例代码的文章就介绍到这了,更多相关vue 虚拟列表内容请搜索代码部落以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码部落!

本文章来源于网络,作者是:coderquan,由代码部落进行采编,如涉及侵权请联系删除!转载请注明出处:https://daimabuluo.cc/JavaScript/2314.html

联系我们

在线咨询:点击这里给我发消息

邮件:dick@daimabuluo.cc

遇到问题?请给我们留言

请填写您的邮箱地址,我们将回复您的电子邮件