造轮子之不同场景下虚拟列表实现

造轮子之不同场景下虚拟列表实现

首页休闲益智音乐滚动之路更新时间:2024-06-28

虚拟列表用来解决大数据量数据渲染问题,由于一次性渲染性能低,所以诞生了虚拟列表渲染。该场景下可视区高度都是相对固定的。相对固定是指在一定条件下可以被改变。

虚拟列表的要做的事是确保性能的前提下,利用一定的技术模拟全数据一次性渲染后效果。

主要通过两件事:

1,渲染数据,适时变更渲染数据

2,模拟滚动效果

场景一,可视区高度固定,单条数据高度固定且相同

比如element-ui el-select下拉选择框,这是最简单的场景。相关参数固定,也最好实现。

<template> <div ref="list" class="render-list-container" @scroll="scrollEvent($event)"> <div class="render-list-phantom" :style="{ height: listHeight 'px' }"></div> <div class="render-list" :style="{ transform: getTransform }"> <template v-for="item in visibleData" > <slot :value="item.value" :height="item.height 'px'" :index="item.id"></slot> </template> </div> </div> </template> <script> export default { name: 'VirtualList', props: { // 所有列表数据 listData: { type: Array, default: () => [] }, // 每项高度 itemSize: { type: Number, default: 50 } }, 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 () { // 当前滚动位置 const scrollTop = this.$refs.list.scrollTop // 此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize) // 此时的结束索引 this.end = this.start this.visibleCount // 此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize) } } } </script> <style scoped> .render-list-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; height: 200px; } .render-list-phantom { position: absolute; left: 0; right: 0; z-index: -1; } .render-list { text-align: center; } </style> 初始化渲染数据

单条数据高度确定,可视区高度确定。可以用二者计算出显示条数。初始索引为0,结束索引为显示条数。之后截取总数据当中对应的数据即可。

// 可显示的列表项数 visibleCount () { return Math.ceil(this.screenHeight / this.itemSize) }, ... mounted () { // 开始索引 this.start = 0 // 结束索引 this.end = this.start this.visibleCount }, 滚动逻辑

可视区高度确定,列表需要模拟出滚动条,这里采用占位div撑开方案。随着滚动条滚动,监听滚动高度计算出开始索引和结束索引,再计算出滚动偏移量,再利用translate3d滚动,translate3d且因为没有重排重绘,所以性能更好。

methods: { scrollEvent () { // 当前滚动位置 const scrollTop = this.$refs.list.scrollTop // 此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize) // 此时的结束索引 this.end = this.start this.visibleCount // 此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize) } } 再渲染逻辑

随着滚动条滚动,监听滚动高度计算出开始索引和结束索引,重置开始和结束索引,也就自然引发Vue重新渲染。

场景二,可视区高度固定,单条数据高度确定但不相同

如果单条数据不固定,一定是因为有不同的数据展示方式,每种方式可以封装成组件,之后动态展示。这块封装了三个组件,高度分别为20px、30px、50px。

封装组件

<template> <div style="height:20px;border:1px solid #333;"> height:20px;---{{ index }} </div> </template> <script> export default { props: ['index'] } </script>

<template> <div style="height:30px;border:1px solid #333;"> height: 30px---{{ index }} </div> </template> <script> export default { props: ['index'] } </script>

<template> <div style="height:50px;border:1px solid #333;"> height: 50px--{{ index }} </div> </template> <script> export default { props: ['index'] } </script> 整体实现

<template> <div ref="list" class="render-list-container" @scroll="scrollEvent($event)"> <div class="render-list-phantom" :style="{ height: listHeight 'px' }" ></div> <div class="render-list" :style="{ transform: getTransform }"> <template v-for="item in visibleData"> <slot :type="item.type" :index="item.id"></slot> </template> </div> </div> </template> <script> export default { name: 'VirtualList', props: { // 所有列表数据 listData: { type: Array, default: () => [] } }, computed: { // 列表总高度 listHeight () { return this.listData.reduce((acc, curVal) => { return acc curVal.height }, 0) }, // 可显示的列表项数 visibleCount () { let accHeight = 0 let count = 0 for (let i = 0; i < this.listData.length; i ) { accHeight = this.listData[i].height if (accHeight >= this.screenHeight) { count break } count } return count }, // 偏移量对应的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.end = this.start this.visibleCount }, data () { return { // 可视区域高度 screenHeight: 0, // 偏移量 startOffset: 0, // 起始索引 start: 0, // 结束索引 end: null } }, methods: { getStart (scrollTop) { var height = 0 var start = 0 var i = 0 while (true) { const currentItem = this.listData[i].height if (currentItem) { height = currentItem if (height >= scrollTop) { start = i break } } else { break } i } return start }, scrollEvent () { // 当前滚动位置 const scrollTop = this.$refs.list.scrollTop // 此时的开始索引 this.start = this.getStart(scrollTop) // 此时的结束索引 this.end = this.start this.visibleCount const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc curVal.height, 0) - this.screenHeight) // 此时的偏移量 this.startOffset = offsetHeight < 0 ? 0 : offsetHeight } } } </script> <style scoped> .render-list-container { overflow: auto; position: relative; -webkit-overflow-scrolling: touch; height: 200px; } .render-list-phantom { position: absolute; left: 0; right: 0; z-index: -1; } .render-list { text-align: center; } </style> 初始化渲染逻辑

通过累加组件高度的方式算出初始化显示条数,初始化开始索引为0,结束索引为显示条数

// 可显示的列表项数 visibleCount () { let accHeight = 0 let count = 0 for (let i = 0; i < this.listData.length; i ) { accHeight = this.listData[i].height if (accHeight >= this.screenHeight) { count break } count } return count }, 滚动逻辑

依然采用占位div撑开方案保证可以滚动。随着滚动条滚动,监听滚动高度,再通过累加方式算出最新的开始索引和结束索引,算出滚动偏移量

methods: { getStart (scrollTop) { var height = 0 var i = 0 while (true) { const currentItem = this.listData[i].height height = currentItem if (height >= scrollTop) { start = i break } i } return i }, scrollEvent () { // 当前滚动位置 const scrollTop = this.$refs.list.scrollTop // 此时的开始索引 this.start = this.getStart(scrollTop) // 此时的结束索引 this.end = this.start this.visibleCount const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc curVal.height, 0) - this.screenHeight) // 此时的偏移量 this.startOffset = offsetHeight < 0 ? 0 : offsetHeight } } 再渲染逻辑

监听滚动高度计算出开始索引和结束索引,重置开始和结束索引,也就自然引发Vue重新渲染。

测试

<template> <div class="render-show"> <div> <VirtualList :listData="data"> <template slot-scope="{type, index}"> <component :is="type" :index="index"></component> </template> </VirtualList> </div> </div> </template> <script> import VirtualList from './parts/VirtualList' import Height20 from './parts/Height20' import Height30 from './parts/Height30' import Height50 from './parts/Height50' const d = [] for (let i = 0; i < 1000; i ) { const type = i % 3 === 0 ? i % 2 === 0 ? 'Height30' : 'Height50' : 'Height20' d.push({ id: i, value: i, type: type, height: type === 'Height30' ? 30 : type === 'Height20' ? 20 : 50 }) } export default { name: 'VirtualList-test', data () { return { data: d } }, components: { VirtualList, Height20, Height30, Height50 } } </script> <style> .render-show { display: flex; justify-content: center; } .render-show > div{ width:500px; margin-top:40px; } .render-list-item { color: #555; box-sizing: border-box; border-bottom: 1px solid #999; box-sizing: border-box; } </style>

VeryCapture_20230901220330.gif

场景三 以上两种情况追加数据

以上两种场景。如何追加数据呢?

scrollEvent () { // 当前滚动位置 const scrollTop = this.$refs.list.scrollTop // 此时的开始索引 this.start = this.getStart(scrollTop) // 此时的结束索引 this.end = this.start this.visibleCount const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc curVal.height, 0) - this.screenHeight) // 此时的偏移量 this.startOffset = offsetHeight < 0 ? 0 : offsetHeight // 追加数据 if (this.end 1 >= this.listData.length) { this.$emit('appendData', this.listData.length) } } ... appendData (start) { const d = [] for (let i = start; i < start 10; i ) { const type = i % 3 === 0 ? i % 2 === 0 ? 'Height30' : 'Height50' : 'Height20' d.push({ id: i, value: i, type: type, height: type === 'Height30' ? 30 : type === 'Height20' ? 20 : 50 }) } this.data = [...this.data, ...d] } 注意事项

以上两个场景中均对偏移量做了处理

this.startOffset = scrollTop - (scrollTop % this.itemSize)

const offsetHeight = scrollTop - (this.visibleData.reduce((acc, curVal) => acc curVal.height, 0) - this.screenHeight) // 此时的偏移量 this.startOffset = offsetHeight < 0 ? 0 : offsetHeight

真实的滚动就是滚动条滚动了多少,可视区就向上移动多少。但虚拟滚动不是的。当起始索引发生变化时,渲染数据发生变化了,但渲染数据的高度不是连续的,所以需要动态的设置偏移量。当滚动时起始索引不发生变化时,此时可以什么也不做,滚动显示的内容由浏览器控制。

关注公众号:通往自由之路pro,了解更多关于个人进步副业投资的心得,共同进步

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved