面试官:给你10万条数据,你怎么处理?
今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)天呐,不会真的有人会直接在页面绘制10万个 DOM 节点来渲染10万条数据吧,这人指定疯了吧!
其实对于十万条数据,我们前端可以采用 虚拟列表 + 分页 的方式来展示
那到底什么是虚拟列表呢?让我们来一探究竟
什么是虚拟列表?
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。来看一下渲染的效果吧:
虚拟列表在线体验地址:www.xyst.club:4888/
一个虚拟列表一共分为上空白区、上缓冲区、可视区、下缓冲区、下空白区,废话不多说,直接上图
假设页面可视区最多能渲染5条数据,这是页面首次加载完渲染的样子:
这是页面发生滚动之后渲染的样子:
这是页面已经滚动到底部该渲染的样子:
代码实现虚拟列表
下面我们用 Vue2 来写前端代码,后端用 express + mockjs 模拟数据返回给前端
这里是后端模拟数据返回给前端,没什么好解释的,直接拷贝用就行
const Mock = require("mockjs")
const express = require("express")
const app = express()
// 设置跨域
app.use(require('cors')())
let length = 0
// 根据传入得 num ,生成 num 条模拟数据
function generatorList(num) {
const mockList = Mock.mock({
[`Mocklist|${num}`]: [{
// 模拟ID 自增方式追加
"id|+1": Number(length),
// 模拟数据内容,中文字符串长度5-10位
info: "@ctitle(20,50)",
}]
})
length += (num - 0)
return mockList
}
// 截取路由发送数据给前端页面
app.get("/getData", function (req, res) {
const { num } = req.query
const data = generatorList(num)
return res.send(data)
})
app.get('/created', function () {
length = 0
})
app.listen(1888, () => {
console.log("本地mock启动,接口地址为:http://localhost:1888/getData?num=请求数量");
})
前端html结构,我们需要写一个滚动盒子和一个上下填充空白的盒子(列表可视区外不需要渲染,我们采用上下内边距padding来填充虚拟节点)
<template>
<div class="VList">
<div class="scroll-box" ref="scrollbox" @scroll.prevent="handlescroll">
<div :style="blankStyle">
<div v-for="item of DOMList" :key="item.id" :index="item.id" class="msg-box">
<div>{{ item.id }}</div>
<div>{{ item.info }}</div>
</div>
</div>
</div>
</div>
</template>
scroll-box: 监听滚动事件的盒子,必须要给此盒子设置高度
blankStyle: 这个盒子主要是用来设置上下内边距的(填充上下空白),style样式为动态绑定
DOM-box: 需要在页面渲染的真实DOM
虚拟列表需要设置的一些数据:
dataList: 总数据列表
DOMList: 需要渲染成DOM的数据列表
oneHeight: 单条数据的高度
maxSize: 可视区最大容积数
firstIndex: DOM节点里第一条数据的索引
endIndex: DOM节点里最后一条数据的索引
blankStyle: 页面上下空白区域的样式
实现思路:
1.计算容器最大容积数量
2.监听滚动事件获取可视区第一条数据的索引firstIndex
3.计算属性计算出最后一条数据的索引endIndex
4.根据 firstIndex 和 endIndex 截取出页面需要渲染的DOM节点数据
优化思路:
- 监听滚动事件设置上下缓冲区消除快速滚动白屏
- 计算属性根据 firstIndex 和 endIndex 动态设置上下空白区域占位
- 监听滚动事件判断是否滚动到底部,这里是采用分页(懒加载)来获取数据,即不用一次性从后端获取十万条数据
最后附上所有代码:
<script>
import axios from 'axios'
export default {
name: 'VirtualList',
data() {
return {
dataList: [], // 总数据列表
oneHeight: 120, // 单条数据的高度
maxSize: '', // 可视区最大容积数
firstIndex: '', // 可视区第一条数据的索引
}
},
methods: {
// 计算列表可视区最大容积
// 思路:用列表可视区高度除以单条数据高度取整加 2
getMaxSize() {
this.maxSize = ~~(this.$refs.scrollbox.offsetHeight / this.oneHeight) + 2
},
// 计算可视区第一条数据的索引
// 思路:用列表向上滚动的距离除以单条数据的高度取整
getFirstIndex() {
this.firstIndex = ~~(this.$refs.scrollbox.scrollTop / this.oneHeight)
},
// 判断页面是否滚动到底部了
isEnd() {
if (this.firstIndex + this.maxSize > this.dataList.length) {
this.getMockData(30) // 滑到底了就拿下一页数据
}
},
// 列表滚动时的回调
handlescroll() {
this.getFirstIndex()
this.isEnd()
},
// 发送请求获取模拟数据
getMockData(num) {
axios({
url: `http://localhost:1888/getData?num=${num}`,
method: 'get'
}).then(res => {
const data = res.data.Mocklist
this.dataList = [...this.dataList, ...data]
})
}
},
mounted() {
this.getMockData(30) // 发送请求获取数据
this.getMaxSize() // 计算列表可视区最大容积
window.onresize = () => this.getMaxSize() // 屏幕大小发生变化时计算一下可视区最大容积数量
},
computed: {
// 页面需要渲染DOM的数据列表
// 思路:DOM列表 = 可视区数据列表 + 上下缓冲区数据列表
DOMList() {
let startIndex = 0
if (this.firstIndex <= this.maxSize) {
startIndex = 0 // DOM节点第一条数据还在缓冲区
} else {
// DOM节点第一条数据超过缓冲区,为虚拟DOM,用空白填充不需要渲染了
startIndex = this.firstIndex - this.maxSize
}
return this.dataList.slice(startIndex, this.endIndex)
},
// DOM数据列表最后一条数据的索引
endIndex() {
let endIndex = this.firstIndex + this.maxSize * 2 // 列表容积 ×2 是为了留下一个下缓冲区
if (!this.dataList[endIndex]) {
endIndex = this.dataList.length // 如果最后一条数据不存在,则直接等于列表所有数据的长度
}
return endIndex
},
// 页面上下空白区域
// 虚拟列表总高度 === 可视区高度 + 上下空白高度
blankStyle() {
let startIndex = 0
if (this.firstIndex <= this.maxSize) {
startIndex = 0 // DOM节点第一条数据还在缓冲区
} else {
startIndex = this.firstIndex - this.maxSize // DOM节点第一条数据超过缓冲区,即要用空白代替
}
return {
paddingTop: startIndex * this.oneHeight + 'px', // 上空白
paddingBottom: (this.dataList.length - this.endIndex) * this.oneHeight + 'px' // 下空白
}
},
},
created() {
axios({
url: 'http://localhost:1888/created',
method: 'get'
})
},
}
</script>
最后最后,再附上样式吧,感兴趣的小伙伴直接Ctrl + C / V 即可直接运行
<style scoped>
.VList {
width: 100vw;
height: 100vh;
}
.scroll-box {
width: 100%;
height: 100%;
overflow-y: auto;
}
.DOM-box {
width: 100%;
height: 120px;
border-bottom: 1px solid #ccc;
}
.DOM-box div {
padding: 0 20px;
}
</style>