列表无限滚动需要考虑两点:
- 数据太多,要做虚拟列表
- 下拉到底,继续加载数据并拼接之前的数据
虚拟列表
只展示可视区域内的列表项目,动态计算可视区域内的列表项,删除非可视区域列表项。
(1)首先确定dom结构
- 第一层作为容器层(infinite-list-container),目的是监听列表滚动,记录滚动位置scrollTop。
- 第二层作为占位层(infinite-list-phantom),根据实际列表的长度占位,撑开空间,形成滚动条
- 第三层作为列表层(infinite-list),列表数据展示的可视化区域,需要用transform:translate3D(x,y,z),这里的y指的是列表偏移量。
(2)监听数据
监听容器的scroll事件,获取滚动位置scrollTop
- 可视区域高度:screenHeight
- 列表每项高度:itemSize
- 列表数据:listData
- 当前滚动位置:scrollTop
(3)确定需要的数据
- 滚动的位置:this.$ref.list.scrollTop
确定列表项的高度:itemSize = 100px
- 可视区域的列表项的总数:visableCount = Math.ceil(screenHeight / itemSize)
- 确定每次加载列表数据的条数: listData.length
- 确定列表的实际的长度:listHeight = itemSize * listData.length
- 开始索引:start = Math.floor(scrollTop / itemSize)
- 结束索引:end = start + visableCount
- 偏移量:scrollTop - (scrollTop % itemSize)
(4)代码
<div class="infinite-list-container" ref="list" @scroll="scrollEvent"> <div class="scrollTopBtn" @click="scrollToTop" v-show="end > 20"> 回到顶部</div> <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div> <div class="infinite-list" :style="{ transform: getTransform }"> <div class="infinite-list-item" v-for="item in visibleData" :key="item.id" @click="toDetail(item.id)" :style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"> <div class="left-section"> {{ item.title[0] }} </div> <div class="right-section"> <div class="title">{{ item.title }}</div> <div class="desc">{{ item.content }}</div> </div> </div> </div> </div>
<script lang="ts"> import { Vue, Component } from "vue-property-decorator"; import Faker from "faker"; interface Data { title: string; content: string; id: number | string; } @Component export default class VirtualList extends Vue { public readonly itemSize: number = 100; public listData: Data[] = []; // 可视区域高度 public screenHeight: number = document.documentElement.clientHeight || document.body.clientHeight; // 可显示的列表项数 public visibleCount: number = Math.ceil(this.screenHeight / this.itemSize); // 偏移量 public startOffset: number = 0; // 起始索引 public start: number = 0; // 结束索引 public end: number = this.start + this.visibleCount; public $refs: { list: any; }; // 列表总高度 get listHeight() { return this.listData.length * this.itemSize; } // 偏移量对应的style get getTransform() { return `translate3d(0,${this.startOffset}px,0)`; } // 获取真实显示列表数据 get visibleData() { return this.listData.slice( this.start, Math.min(this.end, this.listData.length) ); } // 获取数据 getTenListData() { if (this.listData.length >= 200) { return []; } return new Array(10).fill({}).map((item) => ({ id: Faker.random.uuid(), title: Faker.name.title(), content: Faker.random.words(), })); } //初始化 created() { this.listData = this.getTenListData(); } //滚动顶部 scrollToTop() { this.$refs.list.scrollTo({ top: 0, left: 0, behavior: "smooth", }); } //监听滚动事件 public scrollEvent(e: any) { // 当前滚动位置 const scrollTop = this.$refs.list.scrollTop; // 此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize); // 此时的结束索引 this.end = this.start + this.visibleCount; //拼接数据 if (this.end > this.listData.length) { this.listData = this.listData.concat(this.getTenListData()); } // 此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize); } } </script>
(5)图示