有的特殊场景我们不能分页,只能渲染一个长列表。这个长列表中可能有几万条数据,如果全部渲染到页面上用户的设备差点可能就会直接卡死了,这时我们就需要虚拟列表来解决问题。
定高虚拟列表
在定高的虚拟列表中,我们可以根据可视区域的高度和每个 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>
以上,一个定高的虚拟列表已经完成了
下期讲一下非定高虚拟列表