<template>
<div ref="scrollContainer" class="scroll-container" @scroll="throttle(handleScroll)()">
<div ref="listContainer" class="list-container" :style="listStyle">
<div
v-for="(item, index) in visibleItems"
:id="String(item.id)"
:key="index"
class="item"
>
{{ item.content }}
</div>
</div>
</div>
</template>
<script>
const binarySearch = (list, value) => {
let left = 0
let right = list.length - 1
let templateIndex = -1
while (left < right) {
const midIndex = Math.floor((left + right) / 2)
const midValue = list[midIndex].bottom
if (midValue === value) return midIndex + 1
else if (midValue < value) left = midIndex + 1
else if (midValue > value) {
if (templateIndex === -1 || templateIndex > midIndex) templateIndex = midIndex
right = midIndex
}
}
return templateIndex
}
function throttle(fn, delay = 300) {
let canRun = true
return function() {
if (!canRun) return
canRun = false
setTimeout(() => {
fn.apply(this, arguments)
canRun = true
}, delay)
}
}
function loadData(preLen = 0, length = 1000) {
return Array.from({ length }, (_, index) => ({
id: preLen + index,
content: `Item ${preLen + index + 1}` + (Math.random() > 0.5 ? Array.from({ length: Math.floor(Math.random() * 300) }).fill('测试xxx').join(',') : 'aaaa')
}))
}
const estimatedHeight = 50
export default {
data() {
return {
allItems: loadData(),
positions: [],
maxShowCount: 0,
startIndex: 0,
preLen: 0
}
},
computed: {
visibleItems() {
const endIndex = Math.min(this.startIndex + this.maxShowCount, this.allItems.length)
return this.allItems.slice(this.startIndex, endIndex)
},
listStyle() {
const len = this.positions.length
const offsetDis = this.startIndex > 0 ? this.positions[this.startIndex - 1].bottom : 0
const height = len > 0 ? this.positions[len - 1].bottom - offsetDis : 0
return {
height: `${height}px`,
transform: `translateY(${offsetDis}px)`
}
}
},
watch: {
startIndex() {
this.$nextTick(() => {
this.updatePositions()
})
},
allItems(newVal, oldVal) {
if (newVal.length < oldVal.length) {
this.positions = []
this.preLen = 0
this.startIndex = 0
this.$refs.scrollContainer.scrollTop = 0
}
this.initPositions()
this.$nextTick(() => {
this.updatePositions()
})
}
},
mounted() {
this.maxShowCount = Math.ceil(this.$refs.scrollContainer.offsetHeight / estimatedHeight) + 1
this.initPositions()
this.$nextTick(() => {
this.updatePositions()
})
},
methods: {
throttle,
initPositions() {
const diffLen = this.allItems.length - this.preLen
const currentLen = this.positions.length
const preTop = currentLen > 0 ? this.positions[currentLen - 1].top : 0
const preBottom = currentLen > 0 ? this.positions[currentLen - 1].bottom : 0
for (let i = 0; i < diffLen; i++) {
const item = this.allItems[this.preLen + i]
this.positions.push({
index: item.id,
top: preTop ? preTop + i * estimatedHeight : item.id * estimatedHeight,
bottom: preBottom ? preBottom + (i + 1) * estimatedHeight : (item.id + 1) * estimatedHeight,
height: estimatedHeight
})
}
this.preLen = this.allItems.length
},
updatePositions() {
const nodes = [...this.$refs.listContainer.childNodes]
if (nodes.length === 0 || this.positions.length === 0) return
nodes.forEach(node => {
const item = this.positions[+node.id]
item.height = node.clientHeight
})
const startId = +nodes[0].id
for (let i = startId; i < this.positions.length; i++) {
const item = this.positions[i]
item.top = this.positions[i - 1] ? this.positions[i - 1].bottom : 0
item.bottom = item.top + item.height
}
},
handleScroll() {
const { scrollTop, clientHeight, scrollHeight } = this.$refs.scrollContainer
this.startIndex = binarySearch(this.positions, scrollTop)
const bottom = scrollHeight - clientHeight - scrollTop
if (bottom <= 20) {
this.$emit('loadMore')
}
}
}
}
</script>
<style scoped>
.scroll-container {
height: 300px;
overflow-y: auto;
}
.item {
border-bottom: 1px solid #ddd;
text-align: center;
padding: 20px;
}
</style>