虚拟列表
将数据进行切割后根据页面滚动高度分批进行渲染,始终只加载可视区域内的数据。 虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有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>
说明
- bufferedItems :在计算
bufferedItems
时,我们将起始索引向前调整buffer
个项目,结束索引向后调整buffer
个项目。这样可以增加一个缓冲区,防止快速滚动时出现空白。 - translateY:根据调整后的起始索引计算偏移量,确保视图中展示正确的数据。
- spacerHeight:计算顶部空白区域的高度,用于撑开滚动区域。
- handleScroll:滚动事件处理函数,在滚动时更新可见的列表项。
- 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操作次数,提高性能。