当要展示的数据量很大的时候,可以采用分页的方式,但是用户在浏览信息时并不连贯。此时可以将数据缓存在本地,采用虚拟列表的方式展示,这里使用Vue3开发一个简单的虚拟列表。
思路
先说一下思路吧,这不是唯一的实现方式,也不是最好的实现方式,但是原理大同小异,这种方法相对容易理解一些。
页面结构
<div id="app">
<div class="container">
<div class="empty"></div>
<ul class="list">
<li class="item"></li>
</ul>
</div>
</div>
- 初始化容器:给一个容器 container 设置高度,列表将展示在该容器中,设置溢出滚动。
- 撑开容器:设置一个固定行高,就可以根据列表的
length
计算出该列表的实际高度,设置给一个div
此处称为 empty ,放在容器中,这样容器已经可以开始滚动了,只不过页面中没有数据。 - 渲染列表:根据固定行高计算出容器可以展示几个 item ,页面始终展示该固定个数,给 container 设置
display: flex;
,让 list 和 empty 并排展示,由于 empty 只有高度没有宽度,所以对于用户来说是不可见的,只起到撑开 container 的作用。
- 容器滚动:当滚动 container 时,根据
scrollTop
计算出有多少个 item 被滚走了,设置为slice
的start
,重新计算应该截取展示的那一部分,并给 list 设置和scrollTop
相同的translateY
,使列表的顶部始终和容器的顶部对齐。
代码实现
const oriData = Array.from({ length: 1000 }, (v, k) => k)
const itemHeight = 20
const emptyHeight = itemHeight * oriData.length
const containerHeight = window.innerHeight
const itemCount = Math.ceil(containerHeight / itemHeight)
oriData
生成一个长度为1000的数组,当作假数据itemHeight
设置行高为20,即每条数据占用高度emptyHeight
列表实际应该占用的高度containerHeight
容器高度itemCount
页面中每次展示的数量,这里用ceil
是因为假设最后一行不够展示一个 item 了也要展示出来,否则页面最下面会空一块
const container = ref(null)
const start = ref(0)
const translateY = ref(0)
const listData = computed(() => {
return oriData.slice(start.value, start.value + itemCount + 1)
})
translateY
随着页面的滚动, list 也要跟着动,否则就会从屏幕中消失listData
即页面中始终展示的数据,这里设置成计算属性的原因是,列表中的数据会随着屏幕的滚动不断变化
onMounted(() => {
container.value.addEventListener('scroll', e => {
const { scrollTop } = e.target
start.value = Math.floor(scrollTop / itemHeight)
translateY.value = scrollTop + 'px'
})
})
当容器滚动时,获取 scrollTop
,赋给 translateY
,让 list 的顶部始终和 container 顶部对其。并计算出滚动的距离相当于多少个 item ,这里用 floor
的原因是假设滚动了 39px
应该相当于滚动走了一个 item 而不是两个。上面的计算属性会根据 start
的变化重新计算出应该截取的数组,依赖于Vue会使操作更简单。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
}
.container {
overflow: auto;
display: flex;
}
</style>
</head>
<div id="app">
<div class="container" ref="container" :style="{ height: containerHeight }">
<div class="empty" :style="{ height: emptyHeight }"></div>
<ul class="list" :style="{ transform: `translateY(${translateY})` }">
<li v-for="item in listData" :key="item" class="item" :style="{ height: itemHeight }">{{ item }}</li>
</ul>
</div>
</div>
<body>
<script src="https://unpkg.com/vue@next"></script>
<script>
const { ref, onMounted, computed } = Vue
const App = {
setup() {
const oriData = Array.from({ length: 1000 }, (v, k) => k)
const itemHeight = 20
const emptyHeight = itemHeight * oriData.length
const containerHeight = window.innerHeight
const itemCount = Math.ceil(containerHeight / itemHeight)
const container = ref(null)
const start = ref(0)
const translateY = ref(0)
const listData = computed(() => {
return oriData.slice(start.value, start.value + itemCount + 1)
})
onMounted(() => {
container.value.addEventListener('scroll', e => {
const { scrollTop } = e.target
start.value = Math.floor(scrollTop / itemHeight)
translateY.value = scrollTop + 'px'
})
})
return {
listData,
container,
translateY,
containerHeight: containerHeight + 'px',
itemHeight: itemHeight + 'px',
emptyHeight: emptyHeight + 'px'
}
}
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
写在后面
其实根据虚拟DOM的渲染规则,这个示例不给 item 加 key
会比加 key
性能更好