Vue框架下的虚拟列表
原生虚拟列表
固定列表高度的实现
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>.list-wrap{position: relative;overflow-y: scroll;width: 200px;margin: 100px auto;box-sizing: border-box;border: solid 1px red;}.list{position: absolute;top: 0;left: 0;}.list li{border: solid 1px blue;line-height: 30px;}</style></head><body><ul id="app"><div class="list-wrap" ref="listWrap" @scroll="scrollListener"><!-- 利用scroll-bar将盒子的高度撑开 --><div class="scroll-bar" ref="scrollBar"></div><!-- 展示可视化区域的列表 --><ul class="list" ref="list"><li v-for="val in showList">{{val}}</li></ul></div></ul><script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script><script>new Vue({el: '#app',data(){return {list: [],//超长的显示数据itemHeight: 30,//每一列的高度showNum: 10,//显示几条数据start: 0,//滚动过程显示的开始索引end: 10,//滚动过程显示的结束索引}},computed: {//显示的数组,用计算属性计算showList(){return this.list.slice(this.start, this.end);}},mounted(){//构造一个超长列表for (let i = 0; i < 1000000; i++) {this.list.push('列表' + i)}//计算滚动容器高度this.$refs.listWrap.style.height = this.itemHeight * this.showNum + 'px';//计算总的数据需要的高度,构造滚动条高度this.$refs.scrollBar.style.height = this.itemHeight * this.list.length + 'px';},methods: {scrollListener(){//获取滚动高度let scrollTop = this.$refs.listWrap.scrollTop;//开始的数组索引this.start = Math.floor(scrollTop / this.itemHeight);//结束索引this.end = this.start + this.showNum;//绝对定位对相对定位的偏移量this.$refs.list.style.top = this.start * this.itemHeight + 'px';console.log(this.start, this.end)}}})</script></body></html>
不固定列表高度的实现
参考:https://juejin.cn/post/6844903982742110216#heading-0
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><style>html {height: 100%;}body {height: 100%;margin: 0;}#app {height: 100%;}.infinite-list-container {overflow: auto;position: relative;-webkit-overflow-scrolling: touch;}.infinite-list-phantom {position: absolute;left: 0;top: 0;right: 0;z-index: -1;}.infinite-list {left: 0;right: 0;top: 0;position: absolute;}.infinite-list-item {padding: 5px;color: #555;box-sizing: border-box;border-bottom: 1px solid #999;/* height:200px; */}</style></head><body><div id="app"><virtual-list :listdata="data" :estimateditemsize='85' v-slot="slotProps"><item :item="slotProps.item" /></virtual-list></div><script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script><script>// 全局组件Vue.component('virtual-list', {props: {//所有列表数据listdata: {type: Array,default: () => []},//预估高度estimateditemsize: {type: Number,required: true},//容器高度 100px or 50vhheight: {type: String,default: "100%"}},data() {return {//可视区域高度screenHeight: 0,//起始索引start: 0,//结束索引end: 0};},computed: {// 所有的列表数据_listData() {return this.listdata.map((item, index) => {return {_index: `_${index}`,item};});},// 可视区域可以渲染数据的条数visibleCount() {return Math.ceil(this.screenHeight / this.estimateditemsize);},// 可视区域渲染的数据visibleData() {return this._listData.slice(this.start, this.end);}},created() {this.initPositions();},mounted() {this.screenHeight = this.$el.clientHeight;this.start = 0;this.end = this.start + this.visibleCount;},updated() {this.$nextTick(function () {if (!this.$refs.items || !this.$refs.items.length) {// 没有已经渲染的数据则不进行下面的操作return;}//获取真实元素大小,修改对应的尺寸缓存this.updateItemsSize();//更新列表总高度let height = this.positions[this.positions.length - 1].bottom;this.$refs.phantom.style.height = height + "px";//更新真实偏移量this.setStartOffset();});},methods: {initPositions() {// 记录每一条数据的相关信息,每一条数据使用预估高度this.positions = this.listdata.map((d, index) => ({index,height: this.estimateditemsize,top: index * this.estimateditemsize,bottom: (index + 1) * this.estimateditemsize}));},//获取列表起始索引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;},//获取列表项的当前尺寸updateItemsSize() {let nodes = this.$refs.items;nodes.forEach(node => {let rect = node.getBoundingClientRect();let height = rect.height;let index = +node.id.slice(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;}}});},//获取当前的偏移量setStartOffset() {let startOffset =this.start >= 1 ? this.positions[this.start - 1].bottom : 0;this.$refs.content.style.transform = `translate3d(0,${startOffset}px,0)`;},//滚动事件scrollEvent() {//当前滚动位置let scrollTop = this.$refs.list.scrollTop;//此时的开始索引this.start = this.getStartIndex(scrollTop);//此时的结束索引this.end = this.start + this.visibleCount;//此时的偏移量this.setStartOffset();}},template: `<div ref="list" :style="{height}" class="infinite-list-container" @scroll="scrollEvent($event)"><div ref="phantom" class="infinite-list-phantom"></div><div ref="content" class="infinite-list"><divclass="infinite-list-item"ref="items":id="item._index":key="item._index"v-for="item in visibleData"><slot ref="slot" :item="item.item"></slot></div></div></div>`})Vue.component('item', {props: {//所有列表数据item: {type: Object,default: () => {}}},template: `<p><span style="color:red">{{item.id}}</span>{{item.value}}</p>`})new Vue({el: '#app',data() {let data = [];for (let id = 0; id < 1000; id++) {data.push({id,value: 'Earum illum esse dolor. Vel reiciendis nisi sit qui ipsam et. Ea at nihil officiis odio. Iste voluptate earum nobis non architecto id corporis ea. Est ullam repellendus dolor fugit numquam veniam omnis.' // 长文本});}return {// 虚拟滚动的所有列表数据data}},mounted() {}})</script></body></html>
