虚拟滚动列表的思路与实现
/**
* @target 将第三方table组件改造成可无限加载的虚拟列表
* @var 滚动区域 scroll-area
* @var 实际显示区域 real-area
* @var 滚动偏移量 scroll-offset
* @result real-area 设置 scroll-offset,使它一直保持在可视区
*/
<template> | |
<div class="container" ref="containerRef" @scroll="containerScroll"> | |
<div | |
class="content-virtual-scroll" | |
:style="{ height: `${virtualHeight}px` }" | |
> | |
<div | |
class="content-item" | |
:style="{ ...offsetTop(index) }" | |
v-for="(item, index) in realList" | |
@click="$log(item)" | |
> | |
{{ item }} | |
</div> | |
<div class="tips" :style="{...offsetTop(realList.length)}">{{ isLoading ? '正在加载..' : '上拉加载更多' }}</div> | |
</div> | |
</div> | |
</template> | |
<script lang="ts"> | |
import { useRequest } from '@/utils/use-request' | |
import { computed, defineComponent, onMounted, ref, watch } from 'vue' | |
async function request(page = 0) { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
const list: number[] = new Array(20).fill(1).map((_, i) => page * 20 + i) | |
resolve(list) | |
}, 2000) | |
}) | |
} | |
export default defineComponent({ | |
setup() { | |
const itemHeight = 100 | |
const containerRef = ref() | |
const startIndex = ref(0) | |
const scroll = ref(0) | |
const list = ref<number[]>([]) | |
const { request: listRequst, isLoading } = useRequest(request) | |
const virtualHeight = computed(() => list.value.length * itemHeight + 100) | |
const realList = computed(() => | |
list.value.slice(startIndex.value, endIndex.value), | |
) | |
const endIndex = computed(() => { | |
// 8 为每次渲染的最大数量 | |
return startIndex.value + 8 | |
}) | |
watch(endIndex, (val, preVal) => { | |
if (val >= list.value.length) { | |
console.log('to insert') | |
insertList() | |
} | |
}) | |
watch(scroll, (val) => { | |
startIndex.value = Math.floor(val / 100) | |
}) | |
async function insertList() { | |
try { | |
const newList = await listRequst(Math.floor(list.value.length / 20)) | |
list.value.push(...newList) | |
} catch (error) { | |
} | |
} | |
function containerScroll(e: any) { | |
const { scrollTop } = e?.target | |
scroll.value = scrollTop | |
} | |
function offsetTop(index: number) { | |
const y = `${index * 100 + scroll.value - (scroll.value % 100)}px` | |
return { transform: `translateY(${y})` } | |
} | |
onMounted(() => { | |
insertList() | |
}) | |
return { | |
realList, | |
virtualHeight, | |
containerRef, | |
isLoading, | |
offsetTop, | |
containerScroll, | |
} | |
}, | |
}) | |
</script> | |
<style> | |
.container { | |
position: relative; | |
height: 620px; | |
overflow-y: auto; | |
} | |
.content-virtual-scroll { | |
position: absolute; | |
top: 0; | |
right: 0; | |
height: 100%; | |
width: 100%; | |
} | |
.content-item { | |
position: absolute; | |
top: 0; | |
left: 0; | |
height: 100px; | |
width: 100%; | |
text-align: center; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
box-sizing: border-box; | |
background: linear-gradient(to bottom, #fff, teal); | |
} | |
.tips { | |
padding: 30px 0; | |
text-align: center; | |
color: #ddd; | |
font-size: 24px; | |
} | |
</style> |
import { ref } from "vue" | |
export function useRequest(originRequest: (arg: any) => Promise<any>) { | |
const isLoading = ref(false) | |
const isEnded = ref(false) | |
async function request(data: any) { | |
if (isLoading.value) { | |
return Promise.reject('') | |
} | |
isLoading.value = true | |
try { | |
const response = await originRequest(data) | |
isLoading.value = false | |
return response | |
} catch (error) { | |
isLoading.value = false | |
return Promise.reject(error) | |
} | |
} | |
return { | |
isLoading, | |
isEnded, | |
request | |
} | |
} |