console
const ListView = {
name: 'ListView',
template: '#list-template',
props: {
data: {
type: Array,
required: true
},
estimatedItemSize: {
type: Number,
default: 30
},
itemSizeGetter: {
type: Function
}
},
computed: {
contentHeight() {
const { data, lastMeasuredIndex, estimatedItemSize } = this;
let itemCount = data.length;
if (lastMeasuredIndex >= 0) {
const lastMeasuredSizeAndOffset = this.getLastMeasuredSizeAndOffset();
return lastMeasuredSizeAndOffset.offset + lastMeasuredSizeAndOffset.size + (itemCount - 1 - lastMeasuredIndex) * estimatedItemSize;
} else {
return itemCount * estimatedItemSize;
}
}
},
mounted() {
this.updateVisibleData();
},
data() {
return {
lastMeasuredIndex: -1,
startIndex: 0,
sizeAndOffsetCahce: {},
visibleData: []
};
},
methods: {
getItemSizeAndOffset(index) {
const { lastMeasuredIndex, sizeAndOffsetCahce, data, itemSizeGetter } = this;
if (lastMeasuredIndex >= index) {
return sizeAndOffsetCahce[index] || { offset: 0, size: 0 };
}
let offset = 0;
if (lastMeasuredIndex >= 0) {
const lastMeasured = sizeAndOffsetCahce[lastMeasuredIndex];
if (lastMeasured) {
offset = lastMeasured.offset + lastMeasured.size;
}
}
for (let i = lastMeasuredIndex + 1; i <= index && i < data.length; i++) {
const item = data[i];
const size = itemSizeGetter.call(null, item, i);
sizeAndOffsetCahce[i] = {
size,
offset
};
offset += size;
}
this.lastMeasuredIndex = Math.min(index, data.length - 1);
return sizeAndOffsetCahce[index] || { offset: 0, size: 0 };
},
getLastMeasuredSizeAndOffset() {
return this.lastMeasuredIndex >= 0 ? this.sizeAndOffsetCahce[this.lastMeasuredIndex] : { offset: 0, size: 0 };
},
binarySearch(low, high, offset) {
let index;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
const middleOffset = this.getItemSizeAndOffset(middle).offset;
if (middleOffset === offset) {
index = middle;
break;
} else if (middleOffset > offset) {
high = middle - 1;
} else {
low = middle + 1;
}
}
if (low > 0) {
index = low - 1;
}
if (typeof index === 'undefined') {
index = 0;
}
return index;
},
exponentialSearch(scrollTop) {
let bound = 1;
const data = this.data;
const start = this.lastMeasuredIndex >= 0 ? this.lastMeasuredIndex : 0;
while (start + bound < data.length && this.getItemSizeAndOffset(start + bound).offset < scrollTop) {
bound = bound * 2;
}
return this.binarySearch(start + Math.floor(bound / 2), Math.min(start + bound, data.length), scrollTop);
},
findNearestItemIndex(scrollTop) {
const { data } = this;
const lastMeasuredOffset = this.getLastMeasuredSizeAndOffset().offset;
if (lastMeasuredOffset > scrollTop) {
return this.binarySearch(0, this.lastMeasuredIndex, scrollTop);
} else {
return this.exponentialSearch(scrollTop);
}
},
updateVisibleData(scrollTop) {
scrollTop = scrollTop || 0;
const start = this.findNearestItemIndex(scrollTop);
const end = this.findNearestItemIndex(scrollTop + this.$el.clientHeight) + 1;
this.visibleData = this.data.slice(start, Math.min(end + 1, this.data.length));
this.startIndex = start;
this.$refs.content.style.webkitTransform = `translate3d(0, ${ this.getItemSizeAndOffset(start).offset }px, 0)`;
},
handleScroll() {
const scrollTop = this.$el.scrollTop;
this.updateVisibleData(scrollTop);
}
}
};
new Vue({
components: {
ListView
},
methods: {
itemSizeGetter(item) {
return 30 + item.value % 10;
}
},
data() {
const data = [];
for (let i = 0; i < 10000; i++) {
data.push({ value: i });
}
return {
data
};
}
}).$mount('#app')
<script src="//unpkg.com/vue@2.5.15/dist/vue.js"></script>
<script type="text/x-template" id="list-template">
<div
class="list-view"
ref="list"
@scroll="handleScroll">
<div
class="list-view-phantom"
:style="{
height: contentHeight + 'px'
}">
</div>
<div
ref="content"
class="list-view-content">
<div
class="list-view-item"
:style="{
height: getItemSizeAndOffset(startIndex + index).size + 'px'
}"
:key="index"
v-for="(item, index) in visibleData">
{{ item.value }}
</div>
</div>
</div>
</script>
<div id="app">
<template>
<list-view
:item-size-getter="itemSizeGetter"
:estimated-item-size="30"
:data="data"></list-view>
</template>
</div>
.list-view {
height: 400px;
overflow: auto;
position: relative;
color: #333;
border: 1px solid #aaa;
}
.list-view-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-view-content {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.list-view-item {
padding: 5px;
color: #666;
line-height: 30px;
box-sizing: border-box;
}