万条数据如何加载显示(长列表优化)

当后端返回一万条数据,前端如何进行处理?

最开始仅仅是把他当作幽默的段子,实际上在真实的业务需求中,会存在后端返回大量数据,这时就得思考解决方法,最好是合理分页或者使用滚动加载等技术来优化用户体验和性能。

接下来总结才学会的通过虚拟列表实现长列表加载功能。主要实现思路如下:

写一个代表可视区域的div固定高度。通过overflow使其允许纵向y轴滚动。计算可视区域中可以显示的数据条数。用可视区域高度除以单条数据高度就可以得到。监听滚动,当滚动条滚动的时候,计算出被卷起的数据的高度。计算可视区域内数据的起始索引,也就是区域内的第一条数据,用卷起的高度除以单条数据的高度。计算可视区域内数据的结束索引。通过起始索引加上刚刚计算出来的可以显示的数据的条数。取起始索引和结束索引中间的数据渲染到可视化区域。计算起始索引对应的数据在列表中的偏移位置,并设置到列表上。

未做优化

未优化直接循环10000条数据渲染效果,通过F12性能(Performance),录制div加载渲染的时间:

实现的代码包含两个文件一个index.vueItemList.vue组件两个文件,可以看出很简单通过v-if进行10000条数据循环并创建真实的dom,这样会耗费大量的时间绘制渲染dom,我最开始运行下面的代码,电脑风扇,并且js运行阻塞给人一种页面卡死的感觉,滑动列表也会卡顿(重新渲染)。

下面是index.vue代码:

js 复制代码
<template>
    <div class="my-long-list">
        <div id="list" v-for="item in items" :key="item.id">
            <ItemList :item="item"></ItemList>
        </div>
    </div>
 
</template>

<script>
import ItemList from './ItemList.vue';
//获取一个10000的数组
var items = [] ;

for (let i = 0; i < 10000; i++) {
            items.push({
                id: i,
                name: 'item' + i,
                age: i,
                address: 'address' + i
            })
        }
export default {
    data() {
        return {
            items//将10000数组添加到data中
        }
    },
    // 初始化
    mounted() {

    },
    components: {
        ItemList
    }
}
</script>

<style>
.my-long-list{
    width: 500px;
    height: 500px;
    overflow: auto;
    border: 1px solid red;
}
</style>

下面是ItemList.vue组件的代码:

js 复制代码
<template>
    <div class="item-list">
        <div >{{ item.name }}</div>
        <div>年龄:{{ item.age }}</div>
        <div>地址:{{ item.address }}</div>
    </div>
</template>

<script>
export default {
    name: 'ItemList',
    props: {
        item: {
            type: Array,
            default: () => []
        }
    },
}
</script>

<style>
.item-list{
    width: 400px;
    height: 50px;
    border: 1px solid #000;
    background-color: #fff;
    margin: 0 auto;
    text-align: center;
    display: flex;
    align-items: center;
    
justify-content: space-between;
    overflow: hidden;
}
.item-list div{
    margin: 5px;
}
</style>

分析需求

我们先理解页面显示列表的原理,其实不管你加载多少条数据页面只会展示,看见区域的内容,而看到见的区域是有限的,例如我上面的案例,最外层div height:500px,每个ItemList高度50px,那么最多可以展示11个ItemList(最上面展示一半,最下面展示一半),如果我们仅仅加载可以看见的内容那效率是不是就能提升很多。 找到方向那就大胆假设,小心求证,找好切入的点,把大问题简化,先拆分三个组件,index.vue一个生成10000条数据,将数据传入到组件scroller.vue中进行处理。在scroller.vue生成一个可以滚动看空白列表 ,再根据滚动的高度计算需要显示的items数组 将显示的数组截取处理进行显示,而不再显示中的则不获取。ItemList.vue显示需要展示区域。

优化代码

index.vue页面代码如下,引入两个组件ItemListScroller,删除了overflow: auto; 将上下滚动功能交给Scroller进行处理。

js 复制代码
<template>
    <div class="my-long-list">
        <!-- 创建一个没有内容的容器,目的让滚动条在滚动的时候显示  ,-->
         <!-- v-slot 使用插槽,将ItemList组件传入到Scroller组件中-->
        <Scroller 
        v-slot="{ item }"
        :items="items" >
            <ItemList :item="item"></ItemList>
        </Scroller>
    </div>
</template>

<script>
import Scroller from './Scroller.vue';
import ItemList from './ItemList.vue';

var items = [];

for (let i = 0; i < 10000; i++) {
    items.push({
        id: i,
        name: 'item' + i,
        age: i,
        address: 'address' + i
    })
}
export default {
    data() {
        return {
            items //需要展示的数据
        }
    },
    computed: {
    },
    components: {
        ItemList,
        Scroller
    }
}
</script>

<style>
.my-long-list {
    width: 500px;
    height: 500px;
    margin: 0 auto;
    border: 1px solid red;
}
</style>

Scroller.vue代码如下:

js 复制代码
<template>
    <!-- 创建一个容器,在最后设置滚动效果 -->
    <div class="my-scroller-container" @scroll="init" ref="container">
        
    <!-- 创建一个没有内容的容器,目的让滚动条在滚动的时候显示 -->
        <div class="my-scroller-wrapper" :style="{ height: totalSize + 'px' }">
            <!-- 创建一个实际显示区域的容器,循环每条数据 -->
            <div class="my-scroller-item" 
            v-for="poolItem in datalist" 
            :key="poolItem.item.id" :style="{
                transform: `translateY(${poolItem.position}px)`,
            }">
                <slot :item="poolItem.item"></slot>
            </div>
        </div>

    </div>
</template>

<script>
export default {
    props: {
        items: {
            type: Array,
            default: () => []
        }
    },
    data() {
        return{
            // 初始化数据
            datalist:[]
        }
    },
    computed: {
        //根据总条数,计算高度
        totalSize() {
            return this.items.length * 50;
        },
    },
    // 初始化
    mounted() {
        // window.vm = this;
        this.init();
    },
    methods:{
         /**
         * 初始化
         * 重新处理每条数据的位置 和 内容
         * @param {*}
         */
         init() {
            let {minSize,maxSize} = this.setShowNum();
            // 重新处理每条数据的位置 和 内容
            this.datalist = this.items.slice(minSize,maxSize).map((item, index) => {
                return {
                    item,
                    position: minSize*50 + index * 50
                }
            })
            window.vm = this;
            // console.log("items",this.datalist);
        },
        // 根据容器高度计算可显示区域条数
        setShowNum() {
            const scrollTop = this.$refs.container.scrollTop;
            const height = this.$refs.container.clientHeight;
            let minSize = Math.floor(scrollTop/50) ;
            let maxSize = Math.ceil((scrollTop+height)/50);
            // console.log("移动位置和显示容器高度",scrollTop,height,minSize,maxSize);
            return {minSize,maxSize}
        }
        
    }
}
</script>

<style>
.my-scroller-container {
    width: 500px;
    height: 500px;
    overflow: auto;
}
.my-scroller-wrapper {
    position:relative;
}

.my-scroller-item {
    position: absolute;
    width: 100%;
    left: 0;
    top: 0;
}
</style>

ItemList.vue组件代码参考上面修改前(没有改动)。

功能解析

下面详细讲讲实现思路和逻辑, 第一步:首先如上面的代码创建了两个divmy-scroller-containermy-scroller-wrapper并给他们添加样式:my-scroller-container是最终显示的div,添加overflow: auto; 让他内部可以滚动。my-scroller-wrapper是一个空白的div,高度通过计算属性总条数*每条高度计算得到:style="{ height: totalSize + 'px' }"

第二步:通过使用Vue的Refs机制,获取my-scroller-container元素的滚动条位置,并将其存储在变量scrollTop 中。获取my-scroller-container元素高度height 有这两个值可以计算出中间区域需要显示几个ItemList

js 复制代码
        // 根据容器高度计算可显示区域条数
        setShowNum() {
            const scrollTop = this.$refs.container.scrollTop;
            const height = this.$refs.container.clientHeight;
            let minSize = Math.floor(scrollTop/50) ;
            let maxSize = Math.ceil((scrollTop+height)/50);
            // console.log("移动位置和显示容器高度",scrollTop,height,minSize,maxSize);
            return {minSize,maxSize}
        }

通过setShowNum可以计算出当前位置需要渲染的items最小和最大(下标 ),通过init函数计算得到需要渲染的数组,并讲itemList绝对布局,根据数组下标通过计算偏移量position,然后修改transform: translateY(${poolItem.position}px),实现每个itemLis都展示在对应的位置,而隐藏区域则不渲染,最后再添加滚动监听,每次滚动触发@scroll="init",发生改变调用init()函数,重新计算出ItemList位置。

js 复制代码
         /**
         * 初始化
         * 重新处理每条数据的位置 和 内容
         * @param {*}
         */
         init() {
            let {minSize,maxSize} = this.setShowNum();
            // 重新处理每条数据的位置 和 内容
            this.datalist = this.items.slice(minSize,maxSize).map((item, index) => {
                return {
                    item,
                    position: minSize*50 + index * 50
                }
            })
            window.vm = this;
            // console.log("items",this.datalist);
        },

调试技巧

调试,将当前组件绑定到window对象中,比如上面的代码中window.vm = this;,绑定之后可以再控制台直接获取vue组件数据,方便调试。

js 复制代码
 // 初始化
    mounted() {
        // window.vm = this;
        this.init();
    },

查看渲染性能

可以看出渲染响应远远快于未优化前,这种是用空间换时间,虽然每次滑动都会重新计算一次,由于每次只会计算10多条性能压力可以忽悠不计,由于渲染时间短,给用户感受更好,不会卡顿死机。

扩展

最后提第三方组件vue-virtual-scoller,使用它可以快速方便的的实现上面的功能: github.com/Akryum/vue-...

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax