前端面试手写--虚拟列表

目录

一.问题背景

二.代码讲解

三.代码改装

四.代码发布


今天我们来学习如何手写一个虚拟列表,本文将把虚拟列表进行拆分并讲解,然后发布到npm网站上.

一.问题背景

为什么需要虚拟列表 呢?这是因为在面对大量数据 的时候,我们的浏览器会将所有数据都渲染到表格上面,但是渲染极其消耗时间 ,就会出现浏览器卡顿的现象.总的来说就是机器性能不行,需要前端对体验进行优化.

同时呢,我们其实正常人眼睛能看清的程度下,一个屏幕也就20-50行数据,面对10^4以上的数据的时候,如果为了只看这么点数据,而将所有数据都直接渲染,这会在短时间消耗大量的算力.

我们前端对同样数据量的数据获取和数据渲染是两个过程,其中渲染的速度远慢于数据获取.所以我们采用虚拟列表这个技巧,每次计算视口可以容纳几个元素,让后将这些元素从总列表当中计算出来,只渲染这部分可视数据,就将压力分散到各段时间,很大程度上可以降低性能压力.

二.代码讲解

我最开始看的面经,作者是用的Vue2写的(不过我忘记出处了),代码如下:

html 复制代码
 <!-- /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代码,我来解释下其中的特殊之处

javascript 复制代码
this.$el.clientHeight;

这段代码,代表获取当前组件的高度

javascript 复制代码
this.$refs.list.scrollTop;

这串代码,代表获取ref值为list的容器的右侧滑动条距离顶部有多少px

这里面用到的技巧就是,这个虚拟表格看起来是一个高度很高的容器 ,包裹着一个填满了数据的子容器,滑不完

实际上该组件是一个高度很高的空容器 +一个只显示一个屏幕数据量的容器 +绝对定位进行布局

实际效果如下:

三.代码改装

由于现在都在写Vue,切是组合式,虽然框架兼容vue2写法,但是还是习惯Vue3组合式的写法

不同的是,因为我们不能再用this.$el.clientHeight;来获取该组件的高度了,所以我在组件最外层又套了一个div,并打了一个ref,用其的高度来进行代替.

代码如下:

javascript 复制代码
<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

欢迎下载尝试喵!

相关推荐
风口上的猪201515 分钟前
thingboard告警信息格式美化
java·服务器·前端
程序员黄同学20 分钟前
请谈谈 Vue 中的响应式原理,如何实现?
前端·javascript·vue.js
爱编程的小庄1 小时前
web网络安全:SQL 注入攻击
前端·sql·web安全
宁波阿成1 小时前
vue3里组件的v-model:value与v-model的区别
前端·javascript·vue.js
柯腾啊2 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段
Jay丶萧邦2 小时前
el-select:有关多选,options选项值不包含绑定值的回显问题
javascript·vue.js·elementui
weixin_535854222 小时前
oppo,汤臣倍健,康冠科技,高途教育25届春招内推
c语言·前端·嵌入式硬件·硬件工程·求职招聘
扣丁梦想家2 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式
pixle02 小时前
Three.js 快速入门教程【一】开启你的 3D Web 开发之旅
前端·javascript·3d
@PHARAOH2 小时前
HOW - 服务接口超时时间和建议策略
前端·接口·接口超时