轻松实现 Vue.js 虚拟滚动列表组件
基于Vue 2 实现一个虚拟列表组件
今天一直在维护一个老功能,全力提升数以万计的图片性能体验,以确保用户获得更优质的享受。把内容整理了一下,顺便发上来。
本文将用轻松的方式向你展示如何用 Vue.js 封装一个虚拟滚动列表组件,让你的应用轻松应对大量数据展示,提升滚动性能。
为什么要用虚拟滚动呢?想象一下,你的应用需要展示成千上万的数据项,这时候直接渲染所有数据项,浏览器可能会吃不消。虚拟滚动的原理很简单,就是只渲染当前可见的那部分列表项,从而节省资源,提升性能。
好,废话不多说,我们来看看这个虚拟滚动列表组件的代码:
<template>
<div class="virtual-list" @scroll="onScroll">
<!-- 插槽:列表顶部的内容 -->
<slot name="before"></slot>
<!-- spacer div 用于撑开列表的总高度 -->
<div class="virtual-list-spacer" :style="{ height: totalHeight + 'px' }"></div>
<!-- viewport div 用于包含当前可见的项目 -->
<div class="virtual-list-viewport" :style="{ transform: 'translateY(' + viewportOffset + 'px)' }">
<!-- 使用 v-for 渲染可见项目,并为每个项目提供一个插槽 -->
<slot v-for="(item, index) in visibleItems" :item="item" :index="index + startIndex"></slot>
</div>
<!-- 插槽:列表底部的内容 -->
<slot name="after"></slot>
</div>
</template>
<script>
export default {
name: 'VirtualList',
props: {
// items 属性表示要显示的项目数组;
items: {
type: Array,
required: true,
},
// itemHeight 属性表示每个项目的高度(必须是固定的);
itemHeight: {
type: Number,
required: true,
},
// buffer 属性表示要在列表的顶部和底部额外渲染的项目数量,以提高滚动性能。
buffer: {
type: Number,
default: 5,
},
},
data() {
return {
totalHeight: 0,
viewportOffset: 0, // 数据表示滚动容器的当前滚动位置
startIndex: 0, // 数据表示当前可见项目的起始索引。
endIndex: 0, // 数据表示当前可见项目的结束索引。
isDataReady: false,
};
},
computed: {
/**
* @returns {*[]} 返回一个新数组,该数组包含从 startIndex 到 endIndex 的所有项目。
*/
visibleItems() {
return this.items.slice(this.startIndex, this.endIndex + 1);
},
},
watch: {
items() {
this.updateTotalHeight();
this.updateVisibleItems();
},
visibleItems() {
this.$emit('change-activeIndex', this.startIndex + 1);
}
},
mounted() {
this.updateTotalHeight();
// this.updateVisibleItems();
},
methods: {
// 更新列表的总高度(totalHeight 数据)
updateTotalHeight() {
this.totalHeight = this.items.length * this.itemHeight;
},
// 计算当前可见项目的范围(startIndex 和 endIndex)
// 并更新视口偏移(viewportOffset)
updateVisibleItems() {
// 获取当前滚动条的位置
const scrollTop = this.$el.scrollTop;
// 计算当前视口中可见项目的数量(加上缓冲区项目数量)
const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight) + this.buffer;
this.startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
this.endIndex = Math.min(this.items.length - 1, this.startIndex + visibleCount);
// 更新视口偏移(viewportOffset)
this.viewportOffset = this.startIndex * this.itemHeight;
// 检测是否滚动到底部
if (this.$el.scrollHeight - scrollTop <= this.$el.clientHeight + this.itemHeight) {
this.$emit('load-more');
}
},
// 当滚动容器滚动时调用
onScroll() {
// 更新可见项目的范围
this.updateVisibleItems();
},
},
};
</script>
<style scoped>
.virtual-list {
overflow-y: auto;
position: relative;
height: 100%;
}
.virtual-list-spacer {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.virtual-list-viewport {
position: absolute;
left: 0;
right: 0;
}
</style>
现在来逐步解释如何实现 VirtualList
组件:
1. 定义模板结构:
在模板中,我们创建一个带有 virtual-list
类的 div,用作虚拟列表的容器。
我们为其添加一个 @scroll
事件侦听器,以便在滚动时调用 onScroll
方法。
接下来,我们创建一个名为 virtual-list-spacer
的 div,用于撑开容器的高度。
我们还添加了三个插槽:before
、默认插槽和 after
,用于在列表的顶部、中间和底部渲染内容。
<template>
<div class="virtual-list" @scroll="onScroll">
<!-- 插槽:列表顶部的内容 -->
<slot name="before"></slot>
<!-- spacer div 用于撑开列表的总高度 -->
<div class="virtual-list-spacer" :style="{ height: totalHeight + 'px' }"></div>
<!-- viewport div 用于包含当前可见的项目 -->
<div class="virtual-list-viewport" :style="{ transform: 'translateY(' + viewportOffset + 'px)' }">
<!-- 使用 v-for 渲染可见项目,并为每个项目提供一个插槽 -->
<slot v-for="(item, index) in visibleItems" :item="item" :index="index + startIndex"></slot>
</div>
<!-- 插槽:列表底部的内容 -->
<slot name="after"></slot>
</div>
</template>
2. 定义组件属性:
我们需要接收几个属性,以便正确地设置虚拟列表。
items
属性表示要显示的项目数组;
itemHeight
属性表示每个项目的高度(必须是固定的);
buffer
属性表示要在列表的顶部和底部额外渲染的项目数量,以提高滚动性能。
props: {
// items 属性表示要显示的项目数组;
items: {
type: Array,
required: true,
},
// itemHeight 属性表示每个项目的高度(必须是固定的);
itemHeight: {
type: Number,
required: true,
},
// buffer 属性表示要在列表的顶部和底部额外渲染的项目数量,以提高滚动性能。
buffer: {
type: Number,
default: 5,
},
},
3. 定义组件数据:
我们需要存储一些数据,以便在组件中跟踪滚动位置和当前可见项目的范围。
viewportOffset
表示数据表示滚动容器的当前滚动位置;
startIndex
和 endIndex
数据表示当前可见项目的起始和结束索引。
data() {
return {
totalHeight: 0,
viewportOffset: 0, // 数据表示滚动容器的当前滚动位置
startIndex: 0, // 数据表示当前可见项目的起始索引。
endIndex: 0, // 数据表示当前可见项目的结束索引。
};
},
4. 定义计算属性:
我们需要两个计算属性来确定要显示哪些项目以及撑开容器高度的 spacer
div 应具有多大的高度。
visibleItems
计算属性返回一个新数组,返回一个新数组,该数组包含从 startIndex 到 endIndex 的所有项目。
computed: {
visibleItems() {
return this.items.slice(this.startIndex, this.endIndex + 1);
},
},
5. 初始化组件:
在 mounted
生命周期钩子中,我们调用 updateTotalHeight
方法,更新列表的总高度(totalHeight 数据)。
mounted() {
this.updateTotalHeight();
},
6. 定义方法:
我们需要定义几个方法来处理滚动事件、计算可见项目范围等。
-
onScroll
方法:在滚动事件发生时调用。onScroll(e) { // 更新可见项目的范围 this.updateVisibleItems(); },
-
updateVisibleItems
方法:计算当前可见项目的范围(startIndex 和 endIndex),并更新视口偏移(viewportOffset)。// 计算当前可见项目的范围(startIndex 和 endIndex) // 并更新视口偏移(viewportOffset) updateVisibleItems() { // 获取当前滚动条的位置 const scrollTop = this.$el.scrollTop; // 计算当前视口中可见项目的数量(加上缓冲区项目数量) const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight) + this.buffer; this.startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer); this.endIndex = Math.min(this.items.length - 1, this.startIndex + visibleCount); // 更新视口偏移(viewportOffset) this.viewportOffset = this.startIndex * this.itemHeight; // 检测是否滚动到底部 if (this.$el.scrollHeight - scrollTop <= this.$el.clientHeight + this.itemHeight) { this.$emit('load-more'); } },
7. 添加样式:
<style scoped>
.virtual-list {
overflow-y: auto;
position: relative;
height: 100%;
}
.virtual-list-spacer {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.virtual-list-viewport {
position: absolute;
left: 0;
right: 0;
}
</style>
至此,我们已经完成了 VirtualList
组件的实现。现在,您可以将该组件用于任何需要虚拟滚动的场景,以提高大型列表的性能。
8. 使用示例
<template>
<div>
<virtual-list
:items="users"
:item-height="50"
:buffer="3"
@load-more="loadMore"
>
<template #before>
<div class="list-header">User List</div>
</template>
<template v-slot="{ item, index }">
<div style="height: 40px;">
{{item.name}}
</div>
</template>
<template #after>
<div class="list-footer" v-if="loading">Loading...</div>
</template>
</virtual-list>
</div>
</template>
<script>
import VirtualList from '@/views/components/VirtualList';
export default {
components: {
VirtualList,
},
data() {
return {
users: [
// 填充初始用户数据,例如:
{ name: 'User 1' },
{ name: 'User 2' },
{ name: 'User 3' },
// ...
],
loading: false,
};
},
mounted() {
this.loadMore()
},
methods: {
loadMore() {
if (this.loading) {
return;
}
this.loading = true;
// 模拟延迟加载
setTimeout(() => {
for (let i = 0; i < 20;
i++) {
this.users.push({ name: `User ${this.users.length + 1}` });
}
this.loading = false;
console.log(this.users)
}, 1000);
},
},
};
</script>
<style>
.virtual-list {
height: 400px!important;
}
.list-header {
background-color: #f0f0f0;
padding: 10px;
text-align: center;
}
.list-footer {
padding: 10px;
text-align: center;
}
</style>
转载自:https://juejin.cn/post/7216526817372749881