抛出问题:后端未做分页处理,一次性返回十万条数据,作为前端开发工程师,应该如何应对呢?
首先,我们来写个测试案例,模拟后端返回十万条数据,来看一下页面渲染效果。
首先我们用node.js创建一个本地服务器。模拟与后端通信。
const http = require('http');
const port = 8000;
let list = [];
let num = 0;
// create 100,000 records
for (let i = 0; i < 100000; i++) {
num++
list.push({
src: 'https://a.a.com/data',
text: `第 ${num}条数据`,
tid: num
})
}
http.createServer(function (req, res) {
// for Cross-Origin Resource Sharing (CORS)
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
'Access-Control-Allow-Headers': 'Content-Type'
})
res.end(JSON.stringify(list));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
然后调用node,启动该服务
node server
封装一个html文本,作为渲染数据的界面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="container">
</div>
<script src="./index.js"></script>
</body>
</html>
接下来就是解决性能问题了
const getList = () => {
return new Promise((resolve, reject) => {
var ajax = new XMLHttpRequest();
ajax.open('get', 'http://127.0.0.1:8000');
ajax.send();
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
resolve(JSON.parse(ajax.responseText))
}
}
})
}
const container = document.getElementById('container');
const renderList = async() => {
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" ><span>${item.text}</span>`
container.appendChild(div)
})
}
renderList()
通过Google控制台,我们可以清晰的看到页面呈现所需要花费的时间。可以看到,页面加载花了接近10秒,对于用户来说,是种极不友好的体验。
原理:简单的实现快速渲染出效果,原理是用户看到的页面内容只是一部分,后面仍然还在加载中
const renderList = async () => {
const list = await getList()
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total limit)
const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" ><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
}
通过调用setTimeout将任务拆分成一个个独立的小任务,达到第一次加载页面快速渲染。我们来看一下总体加载时间。网络花费时间没有减少多少,但是脚本执行时间减少了快三秒。但是在这里渲染时间以及绘制时间都大量提升了,原因是拆分成一个个任务难以避免的又引起了页面重排重绘导致时间快速的上升,所以相比较直接渲染时间只下降了两秒多。
requestAnimationFrame 和 setTimeout 用法类似,都是起到一个定时器的作用,但是相比较于 setTimeout 而言,它更加适合处理dom的操作,总结一下它的相比较 setTimeout 的优点。
requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
前面的 setTimeout 之所以渲染时间花费太多,就是因为在一帧上处理了太多次的页面重排重绘,而这次我们改一下代码调用 requestAnimationFrame 试试。
const renderList = async () => {
const list = await getList()
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total limit)
const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" ><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
})
}
render(page)
}
看一下执行时间,我们可以看到脚本执行时间也降低了,但是不多可以暂时忽略。重点是渲染绘制两者得到了大幅度的下降。所以总体时间相比较setTimeOut下降了两秒多,这是一个很大的进步。
const renderList = async () => {
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total limit)
const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
const fragment = document.createDocumentFragment()
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" ><span>${item.text}</span>`
fragment.appendChild(div)
}
container.appendChild(fragment)
render(page + 1)
})
}
render(page)
}
因为浏览器缓存的存在,会导致两者时间会越来越低,我们重点看渲染和绘制时间,可以看到两者又下降了一秒多,所以这种方案是非常可行的。
前面的手段是将脚本拆分成一个个子任务去执行,现在我们注意到,每次我们都是新建一个div元素,然后通过appenChild将div插入到元素中。而appenChild是一个昂贵的操作,而如果通过文档片段的话,我们先将生成的div添加到文档片段中,然后操作完成后添加到容器中,这样就只需要做一次重排重绘操作即可。修改代码如下。
其实虚拟滚动才是解决这些性能瓶颈最好的办法,我们可以看到在加载时间和脚本执行时间一样的时候,页面加载的时候所花费的大头全部是在渲染以及绘制上,所以解决渲染绘制才是王道。
在上面我们可以看到,我们是实打实的生成了十万个div,生成这十万个div需要花费时间,而div的 appenchild 操作又会触发重新渲染,这又是个花费时间的点。而如果我们只生成固定的div,只去修改动态的数据会怎么样呢。其实这就是虚拟滚动的思想。
我们知道,浏览器的可视化是一个固定的高度宽度。我们计算出可视化高度宽度,得到我们需要生成的列表,然后通过 transform 开启硬件加速,这个属性并不会引起重排以及重绘。我们来看一下案例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import css -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.13/vue.min.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<title>虚拟滚动原理</title>
</head>
<body>
<div id="app">
<el-row :gutter="10">
<el-col :xs="6" :sm="6" :md="5" :lg="4" :xl="2">
<el-button type="danger" @click="virtualScrolling(20)">20条</el-button>
</el-col>
<el-col :xs="6" :sm="6" :md="5" :lg="4" :xl="2">
<el-button type="primary" @click="virtualScrolling(100)">一百条</el-button>
</el-col>
<el-col :xs="6" :sm="6" :md="5" :lg="4" :xl="2">
<el-button type="success" @click="virtualScrolling(1000)">一千条</el-button>
</el-col>
<el-col :xs="6" :sm="6" :md="5" :lg="4" :xl="2">
<el-button @click="virtualScrolling(100000)">十万条</el-button>
</el-col>
</el-row>
<div class="wrap" @scroll="liScroll">
<ul class="ul_wrap" :style="`height:${ulHei}px`">
<li class="li_item" :style="`height:${liHei}px;transform:translateY(${ScroolNum}px)`"
v-for="item in liList" :key="item">
{{item}}
</li>
</ul>
</div>
</div>
</body>
<style>
.wrap {
height: 400px;
background-color: #fff;
overflow: scroll;
margin-top: 20px;
}
.li_item {
border: 1px red solid;
line-height: 50px;
}
</style>
<script>
new Vue({
el: '#app',
data(){
return {
liHei: 50,//li的高度
ulHei: 480,//ul的高度
liList: [],//真实展示的列表
scrollHei:0,//@scroll事件滚动的top值
ScroolNum: 0,//scrollHei能被li高度取余数的整数值。ScroolNum=scrollHei-(scrollHei%liHei)
showList: 0,//真实展示的条数
tableData: [],//全部数据的集合
lastTime:0,//最后时间
}
},
mounted () {
this.virtualScrolling(100000)
},
methods: {
/**滚动监听 */
liScroll (e) {
if(new Date().getTime()-this.lastTime>40){//设置时间间隔,防止滚动事件高频触发消耗内存资源
this.ele = e;//保存元素,方便重置scrollTop值
this.scrollHei = e.target.scrollTop;//保存滚动条scrollTop值
this.ScroolNum = this.scrollHei - (this.scrollHei % this.liHei);//获取已滚动到页面上方不可见的li元素的总高度(translateY的偏移高度)
let len = this.ScroolNum / this.liHei;//计算已经有多少个li滚动到页面上方(视图上方用户不可见的数量)
this.liList = this.tableData.slice(len, len + this.showList);//每次滚动事件后重新计算展示内容(截取的内容对应全部数据集的部分内容)
this.lastTime=new Date().getTime();//记录最后一次更新时间
}
},
/**初始化数据*/
virtualScrolling (num) {
let arr = [];//初始化数组
for (let i = 0; i < num; i++) {//计算给定数据量
arr.push(i+1)
}
this.tableData = arr;//全部数据集
this.showList = Math.floor(this.ulHei / this.liHei) + 4;//计算真实渲染的列表数量
this.liList = this.tableData.slice(0, this.showList);//初始化可视列表的内容
this.lastTime=new Date().getTime();//记录最后一次更新时间
this.$message({
message: `当前数据为${num}条`,
type: 'success'
});
if (!!this.ele) {//判断监听元素是否保存到ele字段中
this.ele.target.scrollTop = 0;//如果元素存在ele中则将scrollTop初始化为0;
this.ScroolNum=0;//初始化translateY的偏移高度
}
},
}
})
</script>
</html>
实际上只生成了13个 li 标签,相比较其他方案动辄生成10万个div,当然是这种速度更快,并且我们可以发现,前面那种情况页面是非常卡顿的,毕竟塞了这么多个div,而采用虚拟滚动,可以应付百万级别的数据量都不卡顿。
原文 https://yolkpie.github.io/2022/09/15/如何处理后端一次性返回的十万条数据/
在react中是单向数据绑定,而在vue和augular中的特色是双向数据绑定。为什么会选择两种不同的机制呢?我猜测是两种不同的机制有不同的适应场景,查了一些资料后,总结一下。
双向数据绑定是非常重要的特性 —— 将JS模型与HTML视图对应,能减少模板编译时间同时提高用户体验。我们将学习在不使用框架的情况下,使用原生JS实现双向绑定 —— 一种为Object.observe
js判断数据类型的多种方法,主要包括:typeof、instanceof、 constructor、 prototype.toString.call()等,下面就逐一介绍它们的异同。
由于js为弱类型语言拥有动态类型,这意味着相同的变量可用作不同的类型。 typeof 运算符返回一个用来表示表达式的数据类型的字符串,目前typeof返回的字符串有以下这些: undefined、boolean、string、number、object、function、“symbol
在js中我们直接这样写typeof obj===‘object’有什么问题呢?发现Array, Object,null都被认为是一个对象了。如何解决这种情况,能保证判断obj是否为一个对象
js要处理十六进制,十进制,字符之间的转换,发现有很多差不多且书写不正确的方法.一个一个实践才真正清楚如何转换,现在来记录一下它们之间转换的方法。
奇数和偶数的判断是数学运算中经常碰到的问题,这篇文章主要讲解通过JavaScript来实现奇偶数的判断。2种判断方法:求余% 、&1
质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。比如100以内共25个,js实现代码如下。
JavaScript自动类型转换真的非常常见,常用的一些便捷的转类型的方式,都是依靠自动转换产生的。比如 转数字 : + x 、 x - 0 , 转字符串 : \\\"\\\" + x 等等。现在总算知道为什么可以这样便捷转换。
XML是标准通用标记语言 (SGML) 的子集,非常适合 Web 传输。XML 提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。 这篇文章主要介绍Js中实现XML和String相互转化
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!