承接上文,定高虚拟滚动,继续展开。定高虚拟滚动
http://t.csdn.cn/ZSXak
不定高虚拟滚动,顾名思义。每一项的高度不是固定的,是根据实际内容撑开高度不一致的列表项。
所以,想要实现不定高虚拟列表,不同于定高虚拟列表 ,需要:
1、每一项的高度
2、每一项距顶部的高度
3、滚动的列表的总高度
因为上述都不是提前定义的,需要先遍历数组循环项,给每一项添加一个大概的虚拟高度,这样可以将列表项撑开。
在dom实际元素加载后,便可以获取到实际的每一项高度、距顶部高度、列表总高度,这时再将值重新赋回去。
父元素设置 overflow:auto 后 ,列表项滚动,触发@scroll事件,可以获取到不断变化的scrollTop值,因为提前加载项数据是有限的,所以这时需要不断截取数据,并渲染回列表。
数据.slice(startIndex,endIndex)。
根据获取到的动态scrollTop 二分法处理,同虚拟列表项距顶部高度不断作比较arr[mid] 为虚拟列高度的中间项,不断while 循环,利用二分之一将数组分割,减小搜索范围,直到最终定位到 目标 坐标 值,即为 startIndex 起始坐标值
endIndex为 起始坐标值 startIndex + 页面渲染列表量。this.scrollTop重新定位到数组的startIndex位置。
在计算属性中定义列表要循环的数据项,通过对原始数组不断的slice(startIndex,endIndex)进行截取,return获得的即为列表渲染项
<template>
<div id="app">
<input type="text" v-model.number="dataLength">条 Height:{{scrollBarHeight}}
<div class="phantom" ref="scrollsss" @scroll="onScroll">
<!-- 列表总高 -->
<ul :style="{height: scrollBarHeight + 'px'}">
<!-- 列表偏移量 -->
<Item :style="{'transform': `translate3d(0,${scrollTop}px,0)`}" v-for="item in visibleList" :item="item" :index="item.index" :key="item.brandId" @update-height="updateItemHeight"/>
</ul>
</div>
</div>
</template>
<script>
import Item from './Item.vue';
export default {
name: "App",
components: {
Item
},
data() {
return {
estimatedItemHeight: 30, // 每一项假定的高度
visibleCount: 10, // 可视窗口展示项
dataLength: 100, // 模拟列表项
startIndex: 0, // 截取数组的 起始 索引
endIndex: 10, // 截取数组的 结束 索引
scrollTop: 0, // 距离顶部的偏移量
scrollBarHeight: 0, // 虚拟列表高度
bufferItemCount: 4, // 缓冲加载项
dataList: [], // 数据列表
itemHeightCache: [], // 每一项高度
itemTopCache: [] // 每一项距顶部的实际高度
};
},
computed: {
// 截取要展示的数据
visibleList() {
return this.dataList.slice(this.startIndex, this.endIndex + this.bufferItemCount);
}
},
watch: {},
created() {
// 初始化demo的列表数据项
this.dataList = this.getDataList();
this.generateEstimatedItemData();
},
mounted() {},
methods: {
generateEstimatedItemData() {
const estimatedTotalHeight = this.dataList.reduce((pre, current, index)=> {
// 给每一项一个虚拟高度
this.itemHeightCache[index] = this.estimatedItemHeight;
// 给每一项距顶部的虚拟高度
this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight;
return pre + this.estimatedItemHeight;
}, 0);
// 列表总高
this.scrollBarHeight = estimatedTotalHeight;
},
updateItemHeight({index, height}) {
// dom元素加载后得到实际高度 重新赋值回去
this.itemHeightCache[index] = height;
// 重新确定列表的实际总高度
this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
return pre + current;
}, 0)
let newItemTopCache = [0];
for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
// 虚拟每项距顶部高度 + 实际每项高度
newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1]
};
// 获得每一项距顶部的实际高度
this.itemTopCache = newItemTopCache;
},
getDataList() {
console.log(this.dataLength);
// 100展开为数组后为每一条数据添加属性
const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({
index: i,
brandId: i + 1,
name: `第${i + 1}项`,
height: Math.floor(Math.max(Math.random() * 10, 5)) * 10
}));
console.log(newDataList);
return newDataList;
},
// 获取渲染项起始索引
getStartIndex(scrollTop) {
// 每一项距顶部的距离
let arr = this.itemTopCache;
let index = -1;
let left = 0,
right = arr.length - 1,
mid = Math.floor((left + right) / 2);
// 判断 有可循环项时进入
while (right - left > 1) {
/*
二分法:拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较。
arr[mid] 为虚拟列高度的中间项
不断while 循环,利用二分之一将数组分割,减小搜索范围
直到最终定位到 目标index 值
*/
// 目标数在左侧
if (scrollTop < arr[mid]) {
right = mid;
mid = Math.floor((left + right) / 2);
} else if (scrollTop > arr[mid]) {
// 目标数在右侧
left = mid;
mid = Math.floor((left + right) / 2);
} else {
index = mid;
return index;
}
}
index = left;
return index;
},
onScroll() {
console.log(this.$refs.scrollsss.scrollTop);
const scrollTop = this.$refs.scrollsss.scrollTop;
console.log('scrollTop', scrollTop);
let startIndex = this.getStartIndex(scrollTop);
// 如果是奇数开始,就取其前一位偶数
if (startIndex % 2 !== 0) {
this.startIndex = startIndex - 1;
} else {
this.startIndex = startIndex;
}
this.endIndex = this.startIndex + this.visibleCount;
this.scrollTop = this.itemTopCache[this.startIndex] || 0;
}
}
};
</script>
<style scoped>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
.phantom {
border: solid 1px #eee;
margin-top: 10px;
height 600px
overflow auto
}
ul {
background: #ccc;
list-style: none;
padding: 0;
margin: 0;
li {
outline: solid 1px #fff;
&:nth-child(2n) {
background: #fff;
}
}
}
</style>
外部引入组件 Item,列表循环项
<template>
<li :key="item.brandId" :style="{height: `${item.height}px`, 'line-height': `${item.height}px`}" ref="node">
<div>
<div>{{item.name}}</div>
</div>
</li>
</template>
<script>
export default {
props: {
item: {
default() {
return {}
},
type: Object
},
index: Number
},
data() {
return {
}
},
mounted() {
this.$emit('update-height', {height: this.$refs.node.getBoundingClientRect().height, index: this.index})
}
}
</script>