本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
有的特殊场景我们不能分页,只能渲染一个长列表。这个长列表中可能有几万条数据,如果全部渲染到页面上用户的设备差点可能就会直接卡死了,这时我们就需要虚拟列表来解决问题。
定高虚拟列表
在定高的虚拟列表中,我们可以根据可视区域的高度和每个 item 的高度计算得出在可视区域内可以渲染多少个
item。不在可视区域里面的 item 那么就不需要渲染了(不管有几万个还是几十万个item),这样就能解决长列表性能很差的问题啦。
如何实现定高虚拟列表呢?
如何实现滚动条
确定可视区域内有多少元素
确定列表的首位索引和末尾索引
滚动的时候更新首位索引和末尾索引
如何实现滚动条
在 container 里加一个全列表高度的元素 placeholder, 假设每个元素的高度是 100, 滚动的容器的高度 list.length*item.height。其中 placeholder 采用绝对定位,为了不挡住可视区域内渲染的列表,所以将其设置为 z-index: -1
<template> <div class="content" ref="content"> <div class="placeholder" :style="{ height: listHeight + 'px' }"></div> </div></template><script>export default { data() { return { listData: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 9, ], itemSize: 100, } }, computed: { // 滚动条高度 listHeight() { return this.listData.length * this.itemSize }, }, mounted() {}, methods: {},}</script><style scoped lang="scss">.content { height: 100vh; overflow: auto; position: relative;}.placeholder { position: absolute; left: 0; top: 0; right: 0; z-index: -1;}</style>确定可视区域内有多少元素?
通过 Math.ceil(可视区域的高度 / 每个 item 的高度)可以计算容器里渲染多少个 item。为什么是 Math.ceil 呢?因为只要有一个元素漏出来一点点也是算一个元素。那么就可以得到几个变量~
start:首位索引,默认 0
renderCount:可视区域内渲染的 item 数量。
end: 末尾索引,start+renderCount
renderList: 可视区域的列表
<template> <div class="content" ref="content"> <div class="placeholder" :style="{ height: listHeight + 'px' }"></div> <!-- 只渲染可视区域列表数据 --> <div class="card-item" v-for="(item, i) in renderList" :key="i" :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px', backgroundColor: `rgba(0,0,0,${item / 100})`, }" > {{ item + 1 }} </div> </div></template><script>export default { data() { return { listData: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 9, ], itemSize: 100, start: 0, containerHeight: 0, } }, computed: { // 滚动条高度 listHeight() { return this.listData.length * this.itemSize }, // 可视区域的列表 renderList () { return this.listData.slice(this.start, this.end + 1) }, // 获取可视区域一共有多少个元素 renderCount () { return Math.ceil(this.containerHeight / this.itemSize) }, end () { return this.start + this.renderCount }, }, mounted() { // 获取可视区域高度 this.containerHeight = this.$refs.content.clientHeight; }, methods: {},}</script><style scoped lang="scss">.content { height: 100vh; overflow: auto; position: relative;}.placeholder { position: absolute; left: 0; top: 0; right: 0; z-index: -1;}</style>接下来监听滚动事件就可以了重新计算start值,Math.floor(scrollTop / itemSize),为什么是Math.floor?因为如果元素只是向上滚动了一些但是还没有完全滚动上去,state值是不能更新的
<template> <div class="content" ref="content" @scroll="handleScroll($event)"> <div class="placeholder" :style="{ height: listHeight + 'px' }"></div> <!-- 只渲染可视区域列表数据 --> <div class="card-item" v-for="(item, i) in renderList" :key="i" :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px', backgroundColor: `rgba(0,0,0,${item / 100})`, }" > {{ item + 1 }} </div> </div></template><script>export default { data() { return { listData: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 91, 2, 3, 4, 5, 6, 7, 8, 9, ], itemSize: 100, start: 0, containerHeight: 0, } }, computed: { // 滚动条高度 listHeight() { return this.listData.length * this.itemSize }, // 可视区域的列表 renderList () { return this.listData.slice(this.start, this.end + 1) }, // 获取可视区域一共有多少个元素 renderCount () { return Math.ceil(this.containerHeight / this.itemSize) }, end () { return this.start + this.renderCount }, }, mounted() { // 获取可视区域高度 this.containerHeight = this.$refs.content.clientHeight; }, methods: { handleScroll (e) { const scrollTop = e.target.scrollTop; this.start = Math.floor(scrollTop / this.itemSize); this.offset = scrollTop - (scrollTop % this.itemSize); } },}</script><style scoped lang="scss">.content { height: 100vh; overflow: auto; position: relative;}.placeholder { position: absolute; left: 0; top: 0; right: 0; z-index: -1;}</style>到了这里,一个初步的虚拟列表就已经完成了,但是这时会出现一个,滑动的时候会多滚动上去一个元素~为什么会出现这个问题?!
上面步骤中我们用了浏览器的滚动事件更新start,更新到准确的start时,浏览器已经滚动上去一个元素的高度了。也就是我们需要把列表向下偏移一个 item 的高度就行
完整代码
<template> <div class="content" ref="content" @scroll="handleScroll($event)"> <div class="placeholder" :style="{ height: listHeight + 'px' }"></div> <div class="list-wrapper" :style="{ transform: getTransform }"> <!-- 只渲染可视区域列表数据 --> <div class="card-item" v-for="(item, i) in renderList" :key="i" :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px', backgroundColor: `rgba(0,0,0,${item / 100})`, }" > {{ item + 1 }} </div> </div> </div></template><script>export default { data () { return { listData: [1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,9], itemSize: 100, start: 0, containerHeight: 0, offset: 0, } }, computed: { listHeight () { return this.listData.length * this.itemSize }, renderList () { return this.listData.slice(this.start, this.end + 1) }, // 获取可视区域一共有多少个元素 renderCount () { return Math.ceil(this.containerHeight / this.itemSize) }, end () { return this.start + this.renderCount }, getTransform () { return `translate3d(0,${this.offset}px,0)` } }, mounted () { // 获取可视区域高度 this.containerHeight = this.$refs.content.clientHeight; }, methods: { handleScroll (e) { const scrollTop = e.target.scrollTop; this.start = Math.floor(scrollTop / this.itemSize); // 偏移量 this.offset = scrollTop - (scrollTop % this.itemSize); } }}</script><style scoped lang="scss"> .content { height: 100vh; overflow: auto; position: relative; } .placeholder { position: absolute; left: 0; top: 0; right: 0; z-index: -1; }</style>以上,一个定高的虚拟列表已经完成了
下期讲一下非定高虚拟列表
- END -
如果您关注前端 + AI 相关领域可以扫码进群交流
添加小编微信进群😊
关于奇舞团
奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。