基于Vue3的极简虚拟滚动实现方案,无额外插件引用,附有完整demo
不多bb,直接开整
定义变量
const demo = ref(null) // 外框盒子 const showNumber = 20 // 当前视窗展示条数 const itemHeight = 20 // 每一条内容的高度 const data = createData(1000) // 实际数据 let startNum = ref(0) // 当前视窗范围内第一个元素下标 let positionTop = ref(0) // 当前视窗范围内第一个元素偏移量 复制代码
滚动
虚拟滚动第一条件首先是要能滚动,那么外框盒子高度固定,设置overflow: auto;
<div ref="demo" class="scroll-box demo" :style="`height: ${showNumber * itemHeight}px;`" ></div> 复制代码
然后外框盒子内部内置一个高度为理论上渲染全部内容的空div占位,用于展示滚动条
<div :style="`height: ${data.length * itemHeight}px;`" ></div> 复制代码
虚拟
虚拟就是仅渲染当前视窗内的内容,而对于超出的部分则进行移除
<template> <div :style="`top: ${positionTop}px;`"> <div v-for="(item, index) in activeList" :key="item" > {{ item }} </div> </div> </template> <script> const activeList = computed(() => { const start = startNum.value return data.slice(start, start + showNumber) }) </script> 复制代码
什么时候对渲染的数据进行替换?
对外框盒子添加 scroll 的监听事件,在滚动的时候获取scrollTop的值并计算当前视窗范围内第一个元素的下标
const scrollEvent = (event) => { const { scrollTop } = event.target startNum.value = parseInt(scrollTop / itemHeight) positionTop.value = scrollTop } onMounted(() => { demo.value.addEventListener('scroll', scrollEvent) }) onUnmounted(() => { if (!demo.value) return demo.value.removeEventListener('scroll', scrollEvent) demo.value = null }) 复制代码
最后
贴上完整demo
<!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" /> <script src="https://unpkg.com/vue@3.2.27/dist/vue.global.js"></script> <title>VirtualScroll</title> </head> <body> <div id="app"> <div ref="demo" class="scroll-box demo" :style="`height: ${showNumber * itemHeight}px;`" > <div class="scroll-blank" :style="`height: ${data.length * itemHeight}px;`" ></div> <div class="scroll-data" :style="`top: ${positionTop}px;`"> <div v-for="(item, index) in activeList" :key="item" class="scroll-item" > {{ item }} </div> </div> </div> </div> <script> const { computed, onMounted, onUnmounted, ref } = Vue const createData = (length) => { return Object.keys(new Array(length).fill('')) } const App = { setup() { const demo = ref(null) // 外框盒子 const showNumber = 20 // 当前视窗展示条数 const itemHeight = 20 // 每一条内容的高度 const data = createData(1000) // 实际数据 let startNum = ref(0) // 当前视窗范围内第一个元素下标 let positionTop = ref(0) // 当前视窗范围内第一个元素偏移量 // 计算当前视窗内实际要渲染的内容 const activeList = computed(() => { const start = startNum.value return data.slice(start, start + showNumber) }) // 滚动的时候计算当前视窗范围内第一个元素下标 const scrollEvent = (event) => { const { scrollTop } = event.target startNum.value = parseInt(scrollTop / itemHeight) positionTop.value = scrollTop } onMounted(() => { demo.value.addEventListener('scroll', scrollEvent) }) onUnmounted(() => { if (!demo.value) return demo.value.removeEventListener('scroll', scrollEvent) demo.value = null }) return { showNumber, itemHeight, demo, positionTop, data, activeList, } }, } const app = Vue.createApp(App) app.mount('#app') </script> <style> .scroll-box { position: relative; overflow: auto; width: 400px; border: 1px solid rgb(0, 0, 0); } .scroll-data { position: absolute; width: 100%; } .scroll-item { height: 20px; } .scroll-item:hover { background: rgb(104, 111, 211); color: #fff; } </style> </body> </html>
作者:WaitForTheWind
链接:https://juejin.cn/post/7054088878877048869