虚拟列表
https://juejin.im/post/5db684ddf265da4d495c40e5 如有帮助,请感谢掘金作者!!!
前言
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或者部分渲染的技术从而达到极高的渲染性能。
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
实现
- 计算当前可视区域起始数据索引(startIndex)
- 计算当前可视区域结束数据索引(endIndex)
- 计算当前可视区域的数据,并渲染到页面中
- 计算startIndex对应的数据在整个列表中的偏移量startOffset并设置到列表上
<template> <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)"> <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div> <div ref='contentt' class="infinite-list" :style="{ transform: getTransform }"> <div ref="items" class="infinite-list-item" v-for="item in visibleData" :key="item.id" :style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }" >{{ item.value }}</div> </div> </div> </template>
- infinite-list-container为可视区域的容器
- infinite-list-phantom为容器内的占位,高度为总列表高度,用于形成滚动条
- infinite-list为列表项的渲染区域
接着监听infinite-list-container的scroll事件,获取滚动位置scrollTop
- 可视区域高度固定:screenHeight
- 列表项高度固定:itemSize
- 列表数据:listData
- 当前滚动位置:scrollTop
则可推算出:
- 列表总高度
listHeight
= listData.length * itemSize - 可显示的列表项数
visibleCount
= Math.ceil(screenHeight / itemSize) - 数据的起始索引
startIndex
= Math.floor(scrollTop / itemSize) - 数据的结束索引
endIndex
= startIndex + visibleCount - 列表显示数据为
visibleData
= listData.slice(startIndex,endIndex)
当滚动后,由于渲染区域
相对于可视区域
已经发生了偏移,此时我需要获取一个偏移量startOffset
,通过样式控制将渲染区域
偏移至可视区域
中。
- 偏移量
startOffset
= scrollTop - (scrollTop % itemSize);
export default { name:'VirtualList', props: { //所有列表数据 listData:{ type:Array, default:()=>[] }, //每项高度 itemSize: { type: Number, default:200 } }, computed:{ //列表总高度 listHeight(){ return this.listData.length * this.itemSize; }, //可显示的列表项数 visibleCount(){ return Math.ceil(this.screenHeight / this.itemSize) }, //偏移量对应的style getTransform(){ return `translate3d(0,${this.startOffset}px,0)`; }, //获取真实显示列表数据 visibleData(){ return this.listData.slice(this.start, Math.min(this.end,this.listData.length)); } }, mounted() { this.screenHeight = this.$el.clientHeight; this.start = 0; this.end = this.start + this.visibleCount; }, data() { return { //可视区域高度 screenHeight:0, //偏移量 startOffset:0, //起始索引 start:0, //结束索引 end:null, }; }, methods: { scrollEvent() { //当前滚动位置 let scrollTop = this.$refs.list.scrollTop; //此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize); //此时的结束索引 this.end = this.start + this.visibleCount; //此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize); } } };
难点:列表项动态高度 (建议根据链接查看掘金作者讲解)
https://codesandbox.io/s/virtuallist2-1bqk6 线上代码
实现过程
定义组件属性estimatedItemSize
,用于接收预估高度
props: { //预估高度 estimatedItemSize:{ type:Number } }
定义positions
,用于列表项渲染后存储每一项的高度以及位置
信息,
this.positions = [ // { // top:0, // bottom:100, // height:100 // } ];
并在初始时根据estimatedItemSize
对positions
进行初始化。
initPositions(){ this.positions = this.listData.map((item,index)=>{ return { index, height:this.estimatedItemSize, top:index * this.estimatedItemSize, bottom:(index + 1) * this.estimatedItemSize } }) }
由于列表项高度不定,并且我们维护了positions
,用于记录每一项的位置,而列表高度
实际就等于列表中最后一项的底部距离列表顶部的位置。
//列表总高度 listHeight(){ return this.positions[this.positions.length - 1].bottom; }
由于需要在渲染完成
后,获取列表每项的位置信息并缓存,所以使用钩子函数updated
来实现:
updated(){ let nodes = this.$refs.items; nodes.forEach((node)=>{ let rect = node.getBoundingClientRect(); let height = rect.height;//实际高度 let index = +node.id.slice(1)//因为可见数据visibleData中都为_index let oldHeight = this.positions[index].height;//预计高度 let dValue = oldHeight - height;//这意思是不得将预计高度设置的大一些,比item中最高的高度的值还要大 //存在差值 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++){//每foreach一个node节点,都相当于更新一下后续的缓存的节点数据(top,bottom)的值 this.positions[k].top = this.positions[k-1].bottom; this.positions[k].bottom = this.positions[k].bottom - dValue; } } }) }
滚动后获取列表开始索引
的方法修改为通过缓存
获取:
//获取列表起始索引 getStartIndex(scrollTop = 0){ let item = this.positions.find(i => i && i.bottom > scrollTop); return item.index; }
//获取列表起始索引 getStartIndex(scrollTop = 0){ //二分法查找 return this.binarySearch(this.positions,scrollTop) }, //二分法查找 binarySearch(list,value){ let start = 0; let end = list.length - 1; let tempIndex = null; while(start <= end){ let midIndex = parseInt((start + end)/2); let midValue = list[midIndex].bottom; if(midValue === value){ return midIndex + 1; }else if(midValue < value){ start = midIndex + 1; }else if(midValue > value){ if(tempIndex === null || tempIndex > midIndex){ tempIndex = midIndex; } end = end - 1; } } return tempIndex; },
滚动后将偏移量
的获取方式变更:
//滚动事件 scrollEvent() { //当前滚动位置 let scrollTop = this.$refs.list.scrollTop; //此时的开始索引 this.start = this.getStartIndex(scrollTop); //此时的结束索引 this.end = this.start + this.visibleCount; //此时的偏移量 this.setStartOffset(); }
//获取当前的偏移量 setStartOffset() { let startOffset =this.start >= 1 ? this.positions[this.start - 1].bottom : 0; this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`; },
解决滚动过快时,出现短暂白屏现象
定义组件属性bufferScale
,用于接收缓冲区数据
与可视区数据
的比例
props: { //缓冲区比例:缓冲比例(bufferScale)与可视item数量(visibleCount)的乘积决定上下缓冲item的数量(
//临近点情况:当临近滑到顶部或底部时,也就是缓冲的数量大于底部或顶部实际剩余缓冲item的数量时包括滑到顶部或底部时,就没有实际缓冲item数量了,也就是说当滑到顶部时,可见区域下方有缓冲item,当滑到底部时,可见区域上方有缓冲item bufferScale:{ type:Number, default:1 } }
可视区上方渲染条数aboveCount
获取方式如下:
aboveCount(){ return Math.min(this.start,this.bufferScale * this.visibleCount) }
可视区下方渲染条数belowCount
获取方式如下:
belowCount(){ return Math.min(this.listData.length - this.end,this.bufferScale * this.visibleCount); }
真实渲染数据visibleData
获取方式如下:
visibleData(){ let start = this.start - this.aboveCount; let end = this.end + this.belowCount; return this._listData.slice(start, end); }