前端面试手写--虚拟列表
- 创业
- 2025-09-07 09:48:02

目录
一.问题背景
二.代码讲解
三.代码改装
四.代码发布
今天我们来学习如何手写一个虚拟列表,本文将把虚拟列表进行拆分并讲解,然后发布到npm网站上.
一.问题背景为什么需要虚拟列表呢?这是因为在面对大量数据的时候,我们的浏览器会将所有数据都渲染到表格上面,但是渲染极其消耗时间,就会出现浏览器卡顿的现象.总的来说就是机器性能不行,需要前端对体验进行优化.
同时呢,我们其实正常人眼睛能看清的程度下,一个屏幕也就20-50行数据,面对10^4以上的数据的时候,如果为了只看这么点数据,而将所有数据都直接渲染,这会在短时间消耗大量的算力.
我们前端对同样数据量的数据获取和数据渲染是两个过程,其中渲染的速度远慢于数据获取.所以我们采用虚拟列表这个技巧,每次计算视口可以容纳几个元素,让后将这些元素从总列表当中计算出来,只渲染这部分可视数据,就将压力分散到各段时间,很大程度上可以降低性能压力.
二.代码讲解我最开始看的面经,作者是用的Vue2写的(不过我忘记出处了),代码如下:
<!-- /component/HelloWorld.vue --> <template> <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)"> <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }" > </div> <div class="infinite-list" :style="{ transform: getTransform }"> <div ref="items" class="infinite-list-item" v-for="item in visibleData" :key="item.id" :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }" > {{ item.value }} </div> </div> </div> </template> <script> export default { name: "TheVirtualList", props: { //所有列表数据 listData: { type: Array, default: () => [], }, //每项高度 itemSize: { type: Number, default: 200, }, }, computed: { //列表总高度 listHeight() { return this.listData.length * this.itemSize; }, //可显示的列表项数 visibleCount() { return Math.ceil(this.screenHeight / this.itemSize); }, //偏移量对应的style getTransform() { return `translate3d(0,${this.startOffset}px,0)`; }, //获取真实显示列表数据 visibleData() { return this.listData.slice( this.start, Math.min(this.end, this.listData.length) ); }, }, mounted() { this.screenHeight = this.$el.clientHeight; console.log('查看高度',this.screenHeight); this.start = 0; this.end = this.start + this.visibleCount; // console.log(`查看传入组件参数:,${this.itemSize}`); }, data() { return { //可视区域高度 screenHeight: 0, //偏移量 startOffset: 0, //起始索引 start: 0, //结束索引 end: null, }; }, methods: { scrollEvent() { //当前滚动位置 let scrollTop = this.$refs.list.scrollTop; //此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize); //此时的结束索引 this.end = this.start + this.visibleCount; //此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize); console.log('查看滚动位置:',scrollTop); }, }, }; </script> <style scoped> .infinite-list-container { height: 100%; overflow: auto; position: relative; -webkit-overflow-scrolling: touch; } .infinite-list-phantom { position: absolute; left: 0; top: 0; right: 0; /* z-index: -1; */ } .infinite-list { left: 0; right: 0; top: 0; /* position: absolute; */ text-align: center; } .infinite-list-item { padding: 10px; color: #555; box-sizing: border-box; border-bottom: 1px solid #999; } </style>这是一段Vue2代码,我来解释下其中的特殊之处
this.$el.clientHeight;这段代码,代表获取当前组件的高度
this.$refs.list.scrollTop;这串代码,代表获取ref值为list的容器的右侧滑动条距离顶部有多少px
这里面用到的技巧就是,这个虚拟表格看起来是一个高度很高的容器,包裹着一个填满了数据的子容器,滑不完
实际上该组件是一个高度很高的空容器+一个只显示一个屏幕数据量的容器+绝对定位进行布局
实际效果如下:
三.代码改装由于现在都在写Vue,切是组合式,虽然框架兼容vue2写法,但是还是习惯Vue3组合式的写法
不同的是,因为我们不能再用this.$el.clientHeight;来获取该组件的高度了,所以我在组件最外层又套了一个div,并打了一个ref,用其的高度来进行代替.
代码如下:
<template> <div ref="virtualList" style="height: 100%;"> <div ref="list" class="infinite-list-container" @scroll="handleScroll()"> <div class="infinite-list-phantom" :style="{ height: `${listHeight}px` }" > </div> <div class="infinite-list" :style="{ transform: getTransform }"> <div ref="items" class="infinite-list-item" v-for="item in visibleData" :key="item.id" :style="{ height: `${itemSize}px`, lineHeight: `${itemSize}px` }" > {{ item.value }} </div> </div> </div> </div> </template> <script setup> import { onMounted } from 'vue'; import { computed } from 'vue'; import { defineProps ,ref} from 'vue'; const props = defineProps({ //所有列表数据 listData: { type: Array, default: () => [], }, //每项高度 itemSize: { type: Number, default: 200, }, }) const virtualList = ref(null); const list = ref(null) // 虚拟列表的数据结构 const virtualListInfo = ref({ start: 0, end: null, startOffset: 0, }) // 虚拟底板高度 const listHeight = computed(() => { return props.listData.length * props.itemSize }) // 获取偏移 const getTransform = computed(() => { return `translate3d(0,${virtualListInfo.value.startOffset}px,0)`; }) // 一页渲染的元素个数 const visibleCount = computed(() => { return Math.ceil(virtualListInfo.value.screenHeight / props.itemSize); }) // 要渲染的列表 const visibleData = computed(() => { // slice特性,如果第二个参数超过数组长度,那么直接获取到末尾即可 return props.listData.slice(virtualListInfo.value.start,virtualListInfo.value.end) }) const aaa = ref(1) console.log('xxxxx',props,aaa); const handleScroll = ()=> { if (list.value) { let scrollTop = list.value.scrollTop //此时的开始索引 virtualListInfo.value.start = Math.floor(scrollTop / props.itemSize); //此时的结束索引 virtualListInfo.value.end = virtualListInfo.value.start + visibleCount.value; //此时的偏移量 virtualListInfo.value.startOffset = scrollTop - (scrollTop % props.itemSize); console.log('查看滚动位置:',scrollTop); } else { console.log('list未引用'); } } onMounted(() => { virtualListInfo.value.screenHeight = virtualList.value.clientHeight; console.log('查看高度',virtualListInfo.value.screenHeight); virtualListInfo.value.start = 0; virtualListInfo.value.end = virtualListInfo.value.start + visibleCount.value; // console.log(`查看传入组件参数:,${this.itemSize}`); }) </script> <style scoped> .infinite-list-container { height: 100%; overflow: auto; position: relative; -webkit-overflow-scrolling: touch; } .infinite-list-phantom { position: absolute; left: 0; top: 0; right: 0; /* z-index: -1; */ } .infinite-list { left: 0; right: 0; top: 0; /* position: absolute; */ text-align: center; } .infinite-list-item { padding: 10px; color: #555; box-sizing: border-box; border-bottom: 1px solid #999; } </style> 四.代码发布我们封装好了这些组件,但是想要其他人发布,所以我们就需要将其放到公网上.像我们开发项目需要从仓库下载别人的包一样,我们也可以发布自己的包到npm仓库上
你可以参考:如何发布自己的npm包(超详细步骤,博主都在用)_npm 发布-CSDN博客
我将上面的代码发布到我自己的仓库了,名称为:lqd-raw-component
欢迎下载尝试喵!
前端面试手写--虚拟列表由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“前端面试手写--虚拟列表”
上一篇
免费大模型网站
下一篇
Ollama开发指南