虚拟列表的简单实现
本文最后更新于:2022年4月21日 上午
https://mp.weixin.qq.com/s/8uttkBw-dwYXsybKePQ3nA
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>虚拟列表</title>
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#app {
height: 100%;
}
.visible-area{
height: 100%;
}
.infinite-list-container {
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
border: 2px solid black;
}
.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 {
margin: 5px;
color: #555;
/* box-sizing: border-box; */
border:6px solid gray;
/* height:200px; */
}
</style>
</head>
<body>
<div id="app">
<div class="visible-area">
我是可视区域
<virtual-list :list-data="list" :estimated-item-size="100" :buffer-scale="5"></virtual-list>
</div>
</virtual-list>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/Faker/3.1.0/faker.js"></script>
<script type="module">
import Vue from "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js";
Vue.component("virtual-list", {
template: `
<div ref="list" :style="{height}" class="infinite-list-container" @scroll="scrollEvent($event)">
<div ref="phantom" class="infinite-list-phantom"></div>
<div ref="content" class="infinite-list">
<div class="infinite-list-item" ref="items" :id="item.id" :key="item.id" v-for="item in visibleData">
<p><span style="color:red">{{item.id}}</span>{{item.value}}</p>
</div>
</div>
</div>
`,
props: {
//列表数据
listData: {
type: Array,
default: () => [],
},
//预估高度
estimatedItemSize: {
type: Number,
required: true,
},
//缓冲区比例
bufferScale: {
type: Number,
default: 1,
},
//容器高度 100px or 50vh
height: {
type: String,
default: "100%",
},
},
computed: {
// 可视区域内能显示几条数据
visibleCount() {
return Math.ceil(this.screenHeight / this.estimatedItemSize);
},
// 可视区域上方数据条数
// Math.min取缓冲区值
aboveCount() {
return Math.min(this.start, this.bufferScale * this.visibleCount);
},
// 可视区域下方数据条数
belowCount() {
return Math.min(
this.listData.length - this.end,
this.bufferScale * this.visibleCount
);
},
visibleData() {
// 需要渲染的数据 = 显示的数据加上上下缓冲区
let start = this.start - this.aboveCount;
let end = this.end + this.belowCount;
return this.listData.slice(start, end);
},
},
created() {
this.initPositions();
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
updated() {
this.$nextTick(function () {
if (!this.$refs.items || !this.$refs.items.length) {
return;
}
//获取真实元素大小,修改对应的尺寸缓存
this.updateItemsSize();
//更新列表总高度
let height = this.positions[this.positions.length - 1].bottom;
this.$refs.phantom.style.height = height + "px";
//更新真实偏移量
this.setStartOffset();
});
},
data() {
return {
//可视区域高度
screenHeight: 0,
//起始索引
start: 0,
//结束索引
end: 0,
};
},
methods: {
initPositions() {
this.positions = this.listData.map((d, index) => ({
index,
height: this.estimatedItemSize,
top: index * this.estimatedItemSize,
bottom: (index + 1) * this.estimatedItemSize,
}));
},
//获取列表起始索引
getStartIndex(scrollTop = 0) {
//二分法查找
return this.binarySearch(this.positions, scrollTop);
},
binarySearch(list, value) {
let start = 0;
let end = list.length - 1;
let tempIndex = null;
while (start <= end) {
let midIndex = parseInt((start + end) / 2);
let midValue = list[midIndex].bottom;
if (midValue === value) {
return midIndex + 1;
} else if (midValue < value) {
start = midIndex + 1;
} else if (midValue > value) {
if (tempIndex === null || tempIndex > midIndex) {
tempIndex = midIndex;
}
end = end - 1;
}
}
return tempIndex;
},
//更新列表项的当前尺寸
updateItemsSize() {
let nodes = this.$refs.items;
nodes.forEach((node) => {
let rect = node.getBoundingClientRect();
let height = rect.height;
let index = +node.id.slice(1);
let oldHeight = this.positions[index].height;
let dValue = oldHeight - height;
//存在差值
if (dValue) {
this.positions[index].bottom =
this.positions[index].bottom - dValue;
this.positions[index].height = height;
for (let k = index + 1; k < this.positions.length; k++) {
this.positions[k].top = this.positions[k - 1].bottom;
this.positions[k].bottom = this.positions[k].bottom - dValue;
}
}
});
},
//计算偏移量并translate
setStartOffset() {
let startOffset;
if (this.start >= 1) {
let size =
this.positions[this.start].top -
(this.positions[this.start - this.aboveCount]
? this.positions[this.start - this.aboveCount].top
: 0);
startOffset = this.positions[this.start - 1].bottom - size;
} else {
startOffset = 0;
}
this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;
},
//滚动事件
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
// let startBottom = this.positions[this.start - ]
//此时的开始索引
this.start = this.getStartIndex(scrollTop);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.setStartOffset();
},
},
});
new Vue({
el: "#app",
data: {
dataLength: 100000,
list: [],
},
created() {
for (let id = 0; id < this.dataLength; ++id) {
this.list.push({
id,
value: faker.lorem.sentences(),
});
}
},
});
</script>
</body>
</html>
虚拟列表的简单实现
http://yoursite.com/2022/02/24/虚拟列表的简单实现/