虚拟滚动原理图
只有数据再可视区域内时 才会加载 在可视区域中 永远只展示几条数据 对数据多的无线下拉功能做了优化
为什么要使用虚拟滚动
- 如果一直加载到底, 那么最终还是会出现大量的DOM节点,导致滚动不流畅。
- 想要定位到某一个位置的数据会非常困难。
- 滚动条无法正确反映操作者当前浏览的信息在全部列表中的位置。而且大量数据加载,一次给我加载十几条,滚到底太慢了。
父组件
<template>
// 调用子组件 传数组
<myVirtualScroller :items="list"></myVirtualScroller>
</template>
<script>
// 引入子组件
import myVirtualScroller from "./scrollView.vue";
// 模拟一个长列表
const list = [];
// 模拟一万条数据
for (let i = 0; i < 10000; i++) {
list.push({
id: i,
label: `virtual-list ${i}`,
});
}
export default {
components: {
myVirtualScroller,
},
data() {
return {
list: list,
};
},
};
</script>
<style scoped>
.container {
height: 300px;
border: 1px solid #ccc;
}
</style>
子组件
// 传入数据可以实现虚拟滚动事件
// 这是一个封装好的子组件 传入数组进行渲染
<template>
<!-- 最底层的可视区容器 -->
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<!-- 中间的可滚动区域,z-index=-1,高度和真实列表相同,目的是出现相同的滚动条 -->
<div
class="infinite-list-phantom"
:style="{ height: listHeight + 'px' }"
></div>
<!-- 最上层的可视区列表,数据和偏移距离随着滚动距离的变化而变化 -->
<div class="infinite-list" :style="{ transform: getTransform }">
<div
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px' }"
>
{{ item.label }}
</div>
</div>
</div>
</template>
<script>
export default {
name: "MyVirtualList",
props: {
//父组件传过来的数据
items: {
type: Array,
default: () => [],
},
//可视区域每一项的高度 没有传参的话默认高度是100px
itemSize: {
type: Number,
//没有高度的话 高度会默认是100px
default: 100,
},
},
computed: {
//列表总高度
listHeight() {
// 计算当前传值数据的总长度 * 每个盒子的高度 计算出所有数据的整体高度
return this.items.length * this.itemSize;
},
//可视区列表的项数
visibleCount() {
return Math.ceil(this.screenHeight / this.itemSize);
},
//可视区列表偏移距离对应的样式
getTransform() {
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取可视区列表数据
visibleData() {
// 截取出可视区域显示的盒子 进行渲染
return this.items.slice(
this.start,
Math.min(this.end, this.items.length)
);
},
},
mounted() {
// 获取可是区域的高度
this.screenHeight = this.$refs.list.clientHeight;
// 开始
this.start = 0;
// 结束
this.end = this.start + this.visibleCount;
},
data() {
return {
screenHeight: 0, //可视区域高度
startOffset: 0, //偏移距离
start: 0, //起始索引
end: 0, //结束索引
};
},
methods: {
scrollEvent() {
//当前滚动条滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引 使用当前滚动条高度 / 盒子高度 拿到的是当前是哪一个盒子为第一个
this.start = Math.floor(scrollTop / this.itemSize);
//此时的结束索引
//获取到当前第一个盒子的数值 加 计算好可视区域可容纳多少盒子的值
this.end = this.start + this.visibleCount;
//此时的偏移距离
this.startOffset = scrollTop - (scrollTop % this.itemSize);
},
},
};
</script>
<style scoped>
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
}
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.infinite-list-item {
line-height: 50px;
text-align: center;
color: #555;
border: 1px solid #ccc;
box-sizing: border-box;
}
</style>
目前虚拟列表已经有很多知名的库,如 vue-virtual-scroller、vue-virtual-scroll-list、react-virtualized 等,
vue-virtual-scroller 这个虚拟列表库
RecycleScroller组件
适用于列表每一项高度确定的情况,高度可设置成相同,也可单独配置每一项高度
安装
npm install --save vue-virtual-scroller
src/components/virtualRecycleScroller.vue
<template>
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="desc">
{{ item.label }}
</div>
</RecycleScroller>
<!-- items: 需要渲染的列表,itemSize: 列表项的高度,keyField: 列表循环的key值 -->
</template>
<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
export default {
components: {
RecycleScroller
},
props: {
items: Array
}
}
</script>
<style scoped>
.scroller {
height: 100%;
}
.desc {
height: 50px;
line-height: 50px;
text-align: center;
box-sizing: border-box;
border: 1px solid #ccc;
}
</style>
src/App.vue
template>
<div class="container">
<virtual-recycle-scroller :items="list" />
</div>
</template>
<script>
import virtualRecycleScroller from '@/components/virtualRecycleScroller'
// 模拟一个长列表
const list = []
for(let i=0; i<10000; i++) {
list.push({
id: i,
label: `virtual-list ${i}`
})
}
export default {
components: {
virtualRecycleScroller
},
data() {
return {
list: list
}
}
}
</script>
<style scoped>
.container {
height: 300px;
border: 1px solid #ccc;
}
</style>
DynamicScroller组件
适用于列表每一项高度不确定的情况
src/components/virtualDynamicScroller.vue
<template>
<DynamicScroller class="scroller" :items="items" :min-item-size="50">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[item.label]"
:data-index="index"
>
<div class="desc">{{ item.label }}</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
<!-- minItemSize: 列表项初次渲染使用的最小高度-->
<!-- active: 保持视图,防止不必要的重新计算 -->
<!-- sizeDependencies: 影响高度的值,如果发生变化,则重新计算 -->
</template>
<script>
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
export default {
components: {
DynamicScroller,
DynamicScrollerItem
},
props: {
items: Array
}
}
</script>
<style scoped>
.scroller {
height: 100%;
}
.desc {
padding: 12px;
text-align: center;
border: 1px solid #ccc;
}
</style>
src/App.vue
<template>
<div class="container">
<virtual-dynamic-scroller :items="list" />
</div>
</template>
<script>
import virtualDynamicScroller from '@/components/virtualDynamicScroller.vue'
// 模拟一个长列表
const list = []
for(let i=0; i<10000; i++) {
list.push({
id: i,
label: `virtual-scroller ${i}`
})
}
// 模拟一个内容不同的列表项
list[2].label = `virtual-scroller 2 子豪 子豪 子豪 子豪 子豪 子豪 子豪
子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪 子豪`
export default {
components: {
virtualDynamicScroller
},
data() {
return {
list: list
}
}
}
</script>
<style scoped>
.container {
height: 300px;
border: 1px solid #ccc;
}
</style>