<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 { 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, bufferPercent: 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) 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, } }) }, updatePositions() { let nodes = this.$refs.items nodes.forEach(node => { const {height} = node.getBoundingClientRect() const index = node.id - 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 } } }) }, 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>
|