长列表优化

虚拟列表

将数据进行切割后根据页面滚动高度分批进行渲染,始终只加载可视区域内的数据。 虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有10万条记录需要同时渲染,我们屏幕的可见区域的高度为550px,而列表项的高度为55px,则此时我们在屏幕中最多只能看到10个列表项,那么在渲染的时候,我们只需加载可视区的那10条即可。 虚拟列表可以解决一次性渲染数据量过大时,页面卡顿,(比如: table不分页并且一次性加载上万条复杂的数据)

实现思路

将数据进行切割后根据页面滚动高度分批进行渲染,每次只加载可视区域内的数据。

  • 获取起始和结束索引,起始索引Math.floor(scrollTop / itemHeight),结束索引startIdx + showNum
  • 从原生数据中截取可视区域数据list.slice(startIdx, endIdx)
  • 计算偏移量offset = scrollTop - (scrollTop % itemHeight)
  • 监听scroll事件,获取到scrollTop并实时计算可视区域高度
  • 注意可视区域高度应略大于列表组件高度,撑出滚动条,但不应设置过大造成加载数据过多

VirtualList.vue

vue 复制代码
<template>
  <view class="virtual-list" @scroll="handleScroll">
    <!-- 虚拟列表的顶部空白区域,高度等于所有列表项高度之和 -->
    <view class="spacer" :style="{ height: spacerHeight + 'px' }"></view>
    <!-- 实际渲染的列表区域 -->
    <view class="list" :style="{ transform: 'translateY(' + translateY + 'px)' }">
      <!-- 循环渲染可见的列表项 -->
      <view v-for="(item, index) in bufferedItems" :key="index" class="list-item">
        <slot :item="item" :index="startIndex + index"></slot>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    items: {
      type: Array,
      required: true
    },
    itemHeight: {
      type: Number,
      required: true
    },
    visibleItemCount: {
      type: Number,
      required: true
    },
    buffer: {
      type: Number,
      default: 5
    }
  },
  data() {
    return {
      startIndex: 0, // 可见列表项的起始索引
      endIndex: 0, // 可见列表项的结束索引
      scrollTop: 0 // 滚动的距离
    };
  },
  computed: {
    // 计算缓冲区内的列表项
    bufferedItems() {
      const start = Math.max(0, this.startIndex - this.buffer); // 起始索引向前调整 buffer 个项目
      const end = Math.min(this.items.length, this.endIndex + this.buffer); // 结束索引向后调整 buffer 个项目
      return this.items.slice(start, end);
    },
    // 计算列表区域的垂直偏移量
    translateY() {
      return Math.max(0, (this.startIndex - this.buffer) * this.itemHeight);
    },
    // 计算顶部空白区域的高度,用于撑开滚动区域
    spacerHeight() {
      return this.items.length * this.itemHeight;
    }
  },
  methods: {
    // 滚动事件处理函数
    handleScroll(event) {
      this.scrollTop = event.detail.scrollTop;
      this.updateVisibleItems();
    },
    // 更新可见的列表项
    updateVisibleItems() {
      const startIndex = Math.floor(this.scrollTop / this.itemHeight); // 计算起始索引
      const endIndex = startIndex + this.visibleItemCount; // 计算结束索引
      this.startIndex = startIndex; // 更新起始索引
      this.endIndex = Math.min(endIndex, this.items.length); // 更新结束索引
    }
  },
  mounted() {
    this.updateVisibleItems();
  }
};
</script>

<style scoped>
.virtual-list {
  position: relative;
  overflow-y: auto;
  height: 100%;
}
.spacer {
  width: 100%;
}
.list {
  position: absolute;
  width: 100%;
}
.list-item {
  height: 100px; /* 修改为你的默认item高度 */
  width: 100%;
}
</style>

使用虚拟列表组件的示例

vue 复制代码
<template>
  <view class="container">
    <VirtualList :items="items" :itemHeight="100" :visibleItemCount="10" :buffer="5">
      <template v-slot="{ item, index }">
        <view class="item">
          {{ item }}
        </view>
      </template>
    </VirtualList>
  </view>
</template>

<script>
import VirtualList from '@/components/VirtualList.vue';

export default {
  components: {
    VirtualList
  },
  data() {
    return {
      items: []
    };
  },
  created() {
    // 模拟生成大量数据
    for (let i = 0; i < 1000; i++) {
      this.items.push('Item ' + i);
    }
  }
};
</script>

<style>
.container {
  height: 100vh; /* 设置容器高度 */
  overflow-y: auto; /* 启用垂直滚动 */
}
.item {
  height: 100px; /* 确保和 VirtualList 的 itemHeight 一致 */
  display: flex;
  justify-content: center;
  align-items: center;
  border-bottom: 1px solid #ccc;
}
</style>

说明

  1. bufferedItems :在计算 bufferedItems 时,我们将起始索引向前调整 buffer 个项目,结束索引向后调整 buffer 个项目。这样可以增加一个缓冲区,防止快速滚动时出现空白。
  2. translateY:根据调整后的起始索引计算偏移量,确保视图中展示正确的数据。
  3. spacerHeight:计算顶部空白区域的高度,用于撑开滚动区域。
  4. handleScroll:滚动事件处理函数,在滚动时更新可见的列表项。
  5. updateVisibleItems:更新可见的列表项的方法,根据滚动位置计算出起始索引和结束索引。

通过这些详细的注释,可以更清晰地理解虚拟列表组件的实现原理和作用。

分页触底加载

分页加载主要是为了在首次渲染时更快的加载数据,在一些没有分页器但是数据量较多的页面使用。需要后端接口支持分页。

利用 scroll-view 滚动容器将列表项包裹,必须固定 scroll-view 的高度,然后通过 scrolltolower事件监听滚动条触底,触发事件后请求数据,拼接list数组,实现动态渲染列表。

vue 复制代码
<scroll-view v-if="list.length>0" scroll-y @scrolltolower="scrollLower" :style="{height: 'calc(100vh - 136rpx)'}"
    :scroll-top="scrollTop" @scroll="scroll">
    <view class="scroll-wrap">
        <view v-for="(item, index) in list" :key="index">
        </view>
    </view>
    <uni-load-more :status="loadMoreStatus" />
</scroll-view>
<view v-else class="no-data"></view>

<script>
    export default {
        data() {
            return {
                list: [],
                page: 1,
                limit: 30,
                totalPage: 0,
                loadMoreStatus: 'loading', // loading, noMore
                scrollTop: 0,
                old: {
                    scrollTop: 0
                },
            }
        },
        onShow() {
            this.getListData('init')
        },
        methods: {
            // 触底加载
            scrollLower() {
                this.page++
                if (this.page > this.totalPage) {
                    this.loadMoreStatus = 'noMore';
                    return
                }
                this.loadMoreStatus = 'loading'
                this.getListData('append')
            },
            
            // 获取列表数据
            async getListData(type) {
                if (type == 'init') {
                    this.goTop()
                    this.page = 1
                    this.list = []
                }
                let data = {
                    page: this.page,
                    limit: this.limit
                }
                const res = await this.fetchData(data)
                if (res.code == 200) {
                    if (type == 'append') {
                        this.list = this.list.concat(res.data.list)
                    } else {
                        this.list = res.data.list
                    }
                    this.loadMoreStatus = 'noMore';
                    this.totalPage = res.data.totalPage
                }
            },
            
            scroll(e) {
                this.old.scrollTop = e.detail.scrollTop
            },
			
            goTop() {
                this.scrollTop = this.old.scrollTop
                this.$nextTick(function() {
                    this.scrollTop = 0
                })
            }
        }
    }
</script>

分页加载解决数据更新问题

场景:

花材配货任务中,数据量庞大,一天会产生五六百条的数据,故采用分页加载来优化长列表,但不同页的数据项会有更新数据的操作,需要请求接口来刷新列表,通常分页加载场景中,更新数据往往是从第一页开始加载,但如果操作的是第n页的数据,但刷新列表时却回到了第一页,这样的用户体验不好。

为了使得更新数据时不影响其他页的数据,仅更新当前条的数据,采用二维数组来记录每一页的数据,二维数组的索引+1就是页码,当进行更新操作时,请求接口获取该页的数据,然后只替换当前页的数据,这样就不会影响其他页,实现局部的更新,用户体验会更好,需要注意的是在替换二维数组中的某一项时,不能直接通过赋值替换,这样是不具备响应式的,得通过splice方法来替换才具备响应式。

使用场景

分页加载

需要减少列表首次渲染时间 :当列表数据量非常大时,分页加载可以有效减少一次性加载的数据量,降低初始加载时间和内存占用。
数据内容变化频繁 :适用于数据内容经常变化,需要频繁更新的场景。每次加载新的一页数据时,可以更新已有的数据,保持列表的最新状态。
网络请求时间较长:分页加载可以避免一次性请求大量数据导致的长时间等待,用户体验更好。

虚拟列表

数据量非常大 :当数据量非常大,且全部渲染会导致性能问题时,虚拟列表可以显著提升渲染性能和滚动流畅度。
需要平滑滚动体验 :在需要保持平滑滚动体验的场景下,虚拟列表能够减少由于大量DOM节点导致的性能问题。
数据变化不频繁:适用于数据量大但变化不频繁的场景,通过只渲染视口内的元素,减少DOM操作次数,提高性能。

相关推荐
D哈迪斯5 分钟前
vue动态组件实现动态表单的方法
前端·javascript·vue.js
KeyNG_Jykxg5 分钟前
🎨Element Plus X 上新! 组件升级🥳
前端·javascript·vue.js
琦遇27 分钟前
Vue3使用Swiper实现列表内容分两行循环滚动
前端·javascript·vue.js
贰貮27 分钟前
使用Vue 3与.NET 8.0通过SignalR实现实时通信,并结合JWT身份验证
vue.js·websocket·.net·.netcore
罗政1 小时前
AI工具箱源码+成品网站源码+springboot+vue
vue.js·人工智能·spring boot
修复bug1 小时前
利用pnpm patch命令实现依赖包热更新:精准打补丁指南
开发语言·javascript·vue.js
ILUUSION_S1 小时前
Vue接口平台学习七——接口调试页面请求体
vue.js·vue
Aphasia3112 小时前
面试八股文——vue篇📑
前端·vue.js·面试
A尘埃2 小时前
前端ES6基本语法,以及前端项目模板vue-admin-template和后端进行对接(跨域问题的解决)
前端·vue.js·es6·前后端对接
Dignity_呱2 小时前
小小导出,我大前端一人足矣(复杂多级表头)
前端·vue.js·面试