虚拟列表
Nov 23, 2022
todo:待整理
<!-- 虚拟列表:https://juejin.cn/post/7168645862296879117#heading-16 -->
<template>
<div id="app" style="width: 100%">
<div class="container" ref="virtualList">
<div class="phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="content" ref="content" :style="{ transform: `translate3d(0, ${currentOffset}px, 0)` }">
<div v-for="item in visibleData" :key="item.id" :id="item.id" ref="items" class="list-item">
{{ item.value }}
</div>
</div>
</div>
</div>
</template>
<script>
import lodash from 'lodash'
let binarySearch = function (list, target) {
const len = list.length
let left = 0,
right = len - 1
let tempIndex = null
while (left <= right) {
let midIndex = (left + right) >> 1
let midVal = list[midIndex].bottom
if (midVal === target) {
return midIndex
} else if (midVal < target) {
left = midIndex + 1
} else {
// list不一定存在与target相等的项,不断收缩右区间,寻找最匹配的项
if (tempIndex === null || tempIndex > midIndex) {
tempIndex = midIndex
}
right--
}
}
// 如果没有搜索到完全匹配的项 就返回最匹配的项
return tempIndex
}
export default {
name: 'newTest',
data() {
return {
listData: [], // 列表数据
positions: [], // 记录索引 偏移...
preItemSize: 50,
screenHeight: 0,
currentOffset: 0,
start: 0, // 开始
end: 0, // 结束
// count: 0,
bufferPercent: 0.5, // 即每个缓冲区只缓冲 0.5 * 最大可见列表项数 个元素
}
},
created() {
for (let i = 1; i <= 1000; i++) {
this.listData.push({id: i, value: i + '字符内容'.repeat(Math.random() * 20)})
}
this.initPositions(this.listData, this.preItemSize)
},
mounted() {
this.screenHeight = this.$el.clientHeight
this.start = 0
this.end = this.start + this.visibleCount
// 绑定滚动事件
let target = this.$refs.virtualList
let scrollFn = event => this.scrollEvent(event.target)
let debounce_scroll = lodash.debounce(scrollFn, 160)
let throttle_scroll = lodash.throttle(scrollFn, 80)
target.addEventListener('scroll', debounce_scroll)
target.addEventListener('scroll', throttle_scroll)
// target.addEventListener("scroll", scrollFn);
console.log(this.positions, this.visibleData)
},
updated() {
this.$nextTick(() => {
if (!this.$refs.items || !this.$refs.items.length) {
return
}
// 根据真实元素大小,修改对应的缓存列表
this.updatePositions()
// 更新完缓存列表后,重新赋值偏移量
this.currentOffset = this.getCurrentOffset()
})
},
computed: {
listHeight() {
return this.positions[this.positions.length - 1].bottom
},
visibleCount() {
return Math.ceil(this.screenHeight / this.preItemSize)
},
visibleData() {
return this.listData.slice(this.start - this.aboveCount, this.end + this.belowCount)
},
bufferCount() {
return (this.visibleCount * this.bufferPercent) >> 0 // 向下取整
},
// 使用索引和缓冲数量的最小值 避免缓冲不存在或者过多的数据
aboveCount() {
return Math.min(this.start, this.bufferCount)
},
belowCount() {
return Math.min(this.listData.length - this.end, this.bufferCount)
},
},
methods: {
// 滚动回调
scrollEvent(target) {
const {scrollTop} = target
this.start = this.getStartIndex(scrollTop)
this.end = this.start + this.visibleCount
this.currentOffset = this.getCurrentOffset()
},
// 初始化列表
initPositions(listData, itemSize) {
this.positions = listData.map((item, index) => {
return {
index,
top: index * itemSize,
bottom: (index + 1) * itemSize,
height: itemSize,
}
})
},
// 渲染后更新positions
updatePositions() {
let nodes = this.$refs.items
nodes.forEach(node => {
// 获取 真实DOM高度
const {height} = node.getBoundingClientRect()
// 根据 元素索引 获取 缓存列表对应的列表项
const index = node.id - 1
let oldHeight = this.positions[index].height
// dValue:真实高度与预估高度的差值 决定该列表项是否要更新
let dValue = oldHeight - height
// 如果有高度差 !!dValue === true
if (dValue) {
// 更新对应列表项的 bottom 和 height
this.positions[index].bottom = this.positions[index].bottom - dValue
this.positions[index].height = height
// 依次更新positions中后续元素的 top bottom
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
}
}
})
},
getStartIndex(scrollTop = 0) {
return binarySearch(this.positions, scrollTop)
},
getCurrentOffset() {
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)
return this.positions[this.start - 1].bottom - size
} else {
return 0
}
},
},
}
</script>
<style scoped>
.container {
width: 500px;
position: relative;
height: 50vh;
overflow: auto;
}
.phantom {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 800px;
}
.content {
position: absolute;
top: 0;
right: 0;
left: 0;
text-align: center;
}
.list-item {
padding: 10px;
border: 1px solid #999;
}
</style> Back
To Top