当后端返回一万条数据,前端如何进行处理?
最开始仅仅是把他当作幽默的段子,实际上在真实的业务需求中,会存在后端返回大量数据,这时就得思考解决方法,最好是合理分页或者使用滚动加载等技术来优化用户体验和性能。
接下来总结才学会的通过虚拟列表实现长列表加载功能。主要实现思路如下:
写一个代表可视区域的div固定高度。通过overflow使其允许纵向y轴滚动。计算可视区域中可以显示的数据条数。用可视区域高度除以单条数据高度就可以得到。监听滚动,当滚动条滚动的时候,计算出被卷起的数据的高度。计算可视区域内数据的起始索引,也就是区域内的第一条数据,用卷起的高度除以单条数据的高度。计算可视区域内数据的结束索引。通过起始索引加上刚刚计算出来的可以显示的数据的条数。取起始索引和结束索引中间的数据渲染到可视化区域。计算起始索引对应的数据在列表中的偏移位置,并设置到列表上。
未做优化
未优化直接循环10000条数据渲染效果,通过F12
的性能(Performance)
,录制div加载渲染的时间:
实现的代码包含两个文件一个index.vue
和ItemList.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
页面代码如下,引入两个组件ItemList
、Scroller
,删除了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-container
、my-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-...