长列表优化

虚拟列表

将数据进行切割后根据页面滚动高度分批进行渲染,始终只加载可视区域内的数据。 虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有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操作次数,提高性能。

相关推荐
hachi03136 分钟前
el-table-column如何获取行数据的值
javascript·vue.js·elementui
大王棒棒的20 分钟前
五冶项目学习总结
前端·javascript·vue.js
thosefree27 分钟前
SnowAdmin - 功能丰富、简单易用的开源的后台管理框架,基于 Vue3 / TypeScript / Arco Design 等技术栈打造
javascript·vue.js
萌萌哒草头将军2 小时前
🚀🚀🚀 rolldown-vite 实践结果记录,它是真的快!⚡️⚡️⚡️
vue.js·react.js·vite
Dignity_呱2 小时前
vue2和Vue3和React的diff算法展开说说:从原理到优化策略
前端·vue.js·react.js
xjf77113 小时前
前端框架性能综合评估报告:Solid.js、React、Vue与TypeDOM的多维度对比
vue.js·react.js·typescript·前端框架·typedom·solidjs
李q华3 小时前
react与vue的渲染原理
javascript·vue.js·react.js
爱看书的小沐3 小时前
【小沐杂货铺】基于Three.JS绘制太阳系Solar System(GIS 、WebGL、vue、react,提供全部源代码)第2期
javascript·vue.js·gis·webgl·three.js·地球·earth
架构个驾驾3 小时前
Mixin 深度解析与实战指南
前端·javascript·vue.js
前端工作日常3 小时前
同一Vue组件内所有选择器共享同一个 data-v-id
css·vue.js