应用场景
后台一次性发送 10W 条或更多数据给前台,在接口不支持分页的情况下全部渲染,保证用户使用不卡顿。
问题分析
浏览器需要生成 10W 个 DOM 节点用来染页面,必然会导致页面的卡顿
解决方案
虚拟列表: 浏览器只需要生成少量 DOM 元素(节点数量取决于前端视图需要展示的数量),随着页面滚动,不断复用当前这一屏幕的 DOM 元素即可总结: 虚拟列表就是固定 DOM 节点数量,通过修改 DOM 元素的内容而达到不重新增加或删除 DOM 节点实现列表的更新。
实现原理
监听页面滚动,获取滚动的高度 scrollTop
根据 scrollTop 计算出当前应该展示哪一段数据,计算出 startlndex 和 endlndex根据当前展示的数据在长列表中的 index,对列表进行偏移
思路
1.封装组件InfiniteScroll
结构:
外层容器 >内容列表 >作用域插槽 slot
2.外层容器宽度固定,设置 overflow-y: scroll
3.接收参数
itemHeight: 每个条目的高度,用于动态列表的总高度
start: 开始的索引
end: 结束的索引
list: 超大数据,数组格式
4.onMounted 时计算外层容器高度和内容列表高度
5.定义计算属性 showList ,裁切 start - end 之间的数据
6.根据 showList 染 slot
7.监听外层容器的 scroll 事件
获取外层容器的 scrollTop
重新计算 start 和 end
start和 end 的边界外理
同时设置内容列表的 transform 和 height ,此消彼长
代码
InfiniteScroll.vue :
<template>
<div ref="container" class="list-container" :style="{ height: props.scrollHeight + 'px' }" @scroll="handleScroll">
<div ref="content" class="list-content">
<slot v-for="item in showList" :info="item" :key="item"></slot>
</div>
</div>
</template>
<script setup>
import { computed, ref, defineProps } from 'vue'
const container = ref(null)
const content = ref(null)
const props = defineProps(['list', 'scrollHeight', 'start', 'end', 'itemHeight'])
const { list } = props
const itemHeight = ref(props.itemHeight)
const start = ref(props.start)
const end = ref(props.end)
const showLineNum = end.value
const showList = computed(() => list.slice(start.value, end.value))
const handleScroll = () => {
const scrollTop = container.value.scrollTop
start.value = Math.floor(scrollTop / itemHeight.value)
end.value = start.value + showLineNum
if (end.value >= list.length) {
start.value = list.length - showLineNum
end.value = list.length
}
content.value.style.transform = `translateY(${start.value * itemHeight.value}px)`
}
</script>
<style scoped>
.list-container {
width: 400px;
overflow-y: scroll;
}
</style>
App.vue :
<template>
<InfiniteScroll :itemHeight="itemHeight" :scrollHeight="scrollHeight" :start="0" :end="showLineNum"
:list="list">
<template v-slot="{ info }">
<div class="content" :style="{ height: height + 'px' }">{
{ info }}</div>
</template>
</InfiniteScroll>
</template>
<script setup>
import { ref, computed } from 'vue'
import InfiniteScroll from './components/InfiniteScroll.vue';
const list = ref([])
const itemHeight = ref(50)
const showLineNum = computed(() => {
return Math.ceil(window.innerHeight / itemHeight.value) + 1
})
const scrollHeight = computed(() => window.innerHeight)
list.value = new Array(100000).fill(' ').map((item, index) => 'HelloWorld' + index)
</script>
<style>
body,
#app {
height: 100vh;
overflow: hidden;
}
.content {
width: 100%;
border: 2px solid black;
line-height: 50px;
text-indent: 10px;
box-sizing: border-box;
}
</style>